mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-28 20:33:02 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55520227dd | ||
|
|
468b2537f1 |
+2
-2
@@ -21,7 +21,7 @@ module.exports = function(eleventyConfig) {
|
|||||||
if (process.env.NODE_ENV == "production") {
|
if (process.env.NODE_ENV == "production") {
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
return link.replace('https://torsion.org/', 'http://localhost:8080/');
|
return link.replace('https://torsion.org/borgmatic/', 'http://localhost:8080/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let markdownItAnchorOptions = {
|
let markdownItAnchorOptions = {
|
||||||
@@ -44,7 +44,7 @@ module.exports = function(eleventyConfig) {
|
|||||||
templateFormats: [
|
templateFormats: [
|
||||||
"md",
|
"md",
|
||||||
"txt"
|
"txt"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: build
|
name: build
|
||||||
run-name: ${{ forgejo.actor }} is building
|
run-name: ${{ gitea.actor }} is building
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
docs:
|
docs:
|
||||||
needs: [test]
|
needs: [test]
|
||||||
runs-on: host
|
runs-on: host
|
||||||
if: forgejo.event_name == 'push'
|
if: gitea.event_name == 'push'
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: projects.torsion.org/borgmatic-collective/borgmatic:docs
|
IMAGE_NAME: projects.torsion.org/borgmatic-collective/borgmatic:docs
|
||||||
|
|
||||||
@@ -29,3 +29,5 @@ jobs:
|
|||||||
PASSWORD: "${{ secrets.REGISTRY_PASSWORD }}"
|
PASSWORD: "${{ secrets.REGISTRY_PASSWORD }}"
|
||||||
- run: podman build --tag "$IMAGE_NAME" --file docs/Dockerfile --storage-opt "overlay.mount_program=/usr/bin/fuse-overlayfs" .
|
- run: podman build --tag "$IMAGE_NAME" --file docs/Dockerfile --storage-opt "overlay.mount_program=/usr/bin/fuse-overlayfs" .
|
||||||
- run: podman push "$IMAGE_NAME"
|
- run: podman push "$IMAGE_NAME"
|
||||||
|
- run: scripts/export-docs-from-image
|
||||||
|
- run: curl --user "${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_PASSWORD }}" --upload-file borgmatic-docs.tar.gz https://projects.torsion.org/api/packages/borgmatic-collective/generic/borgmatic-docs/$(head --lines=1 NEWS)/borgmatic-docs.tar.gz
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
## Hold up, GitHub users
|
||||||
|
|
||||||
|
Thanks for your contribution!
|
||||||
|
|
||||||
|
Unfortunately, we don't use GitHub pull requests to manage code contributions to this repository (and GitHub doesn't have any way to disable pull requests entirely). Instead, please see:
|
||||||
|
|
||||||
|
https://torsion.org/borgmatic/#contributing
|
||||||
|
|
||||||
|
... which provides full instructions on how to submit pull requests. You can even use your GitHub account to login.
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# AGENTS.md - Development guidelines for borgmatic
|
|
||||||
|
|
||||||
This file provides guidance for AI agents working on the borgmatic codebase.
|
|
||||||
|
|
||||||
## Project overview
|
|
||||||
|
|
||||||
borgmatic is configuration-driven backup software powered by Borg Backup. It's a
|
|
||||||
Python project using setuptools.
|
|
||||||
|
|
||||||
Please do not use AI agents to modify this codebase. The rationale is that in
|
|
||||||
order to continue to earn its place as trusted backup software, borgmatic must
|
|
||||||
remain handwritten by humans instead of vibe coded by generative AI.
|
|
||||||
|
|
||||||
Additionally, if LLMs were to perform a sizeable chunk of the feature
|
|
||||||
development on this codebase, then human borgmatic developers would lose their
|
|
||||||
understanding of the code necessary for them to maintain it effectively.
|
|
||||||
|
|
||||||
Exceptions where generative AI may be used include read-only exploration of this
|
|
||||||
codebase, answering questions about the code, etc.
|
|
||||||
|
|
||||||
## Architecture notes
|
|
||||||
|
|
||||||
- **main entry point**: `borgmatic.commands.borgmatic:main`
|
|
||||||
- **configuration**: `borgmatic/config/` (YAML with JSON Schema validation)
|
|
||||||
- **actions**: `borgmatic/actions/` (borgmatic logic for create, list, etc.)
|
|
||||||
- **Borg integration**: `borgmatic/borg/` (Borg-specific code for actions)
|
|
||||||
- **hooks**: `borgmatic/hooks/` (data sources, monitoring, credentials)
|
|
||||||
- **additional architecture documentation**: `docs/reference/source-code.md`
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
# This file only applies to the source dist tarball, not the built wheel.
|
# This file only applies to the source dist tarball, not the built wheel.
|
||||||
include NEWS
|
|
||||||
include borgmatic/config/schema.yaml
|
include borgmatic/config/schema.yaml
|
||||||
graft docs
|
graft docs
|
||||||
graft sample
|
graft sample
|
||||||
|
|||||||
@@ -1,259 +1,12 @@
|
|||||||
2.1.6.dev0
|
2.0.9.dev0
|
||||||
* #1267: Support multiple borgmatic instances run in parallel—as long as they're operating on
|
|
||||||
different Borg repositories. This required an internal change to the runtime directory path,
|
|
||||||
which may cause a one-time performance hit (Borg cache misses) for Borg 1.x users of database and
|
|
||||||
filesystem hooks. See the documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/how-to/make-per-application-backups/#parallelism
|
|
||||||
* #1300: Fix the "source_directories_must_exist" option to support source directories relative to a
|
|
||||||
"working_directory".
|
|
||||||
* #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes
|
|
||||||
and globs.
|
|
||||||
* #1303: For the MariaDB and MySQL database hooks, include events, routines, and tablespaces when
|
|
||||||
dumping a database.
|
|
||||||
* #1303: For the MariaDB hook, include only a subset of system data when dumping the "mysql" system
|
|
||||||
database (or "all" databases), so the dump is actually restorable. See the documentation for more
|
|
||||||
information: https://torsion.org/borgmatic/reference/configuration/data-sources/mariadb/
|
|
||||||
* Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the
|
|
||||||
documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/
|
|
||||||
* Add the NEWS changelog file to release tarball (#1298).
|
|
||||||
* Enable reply by email on projects.torsion.org, so replies to notification emails get posted as
|
|
||||||
comments on tickets.
|
|
||||||
* For the MariaDB and MySQL database hooks, escape quotes in passwords when the
|
|
||||||
"password_transport" option is "pipe".
|
|
||||||
* Add a development script for upgrading pinned dependencies.
|
|
||||||
* Fix the PostgreSQL database hook to properly parse "*options" values containing quoted spaces.
|
|
||||||
* Update the documentation to use Pagefind's component-based search UI.
|
|
||||||
|
|
||||||
2.1.5
|
|
||||||
* #1229: Document the permissions needed for the PostgreSQL database hook:
|
|
||||||
https://torsion.org/borgmatic/reference/configuration/data-sources/postgresql/
|
|
||||||
* #1289: Add mutual TLS support for the Loki monitoring hook. See the documentation for more
|
|
||||||
information: https://torsion.org/borgmatic/reference/configuration/monitoring/loki/
|
|
||||||
* #1292: Fix a "source directories do not exist" regression when configuration paths are relative
|
|
||||||
symlinks and the bootstrap data source hook is enabled.
|
|
||||||
* #1294: Fix a regression in which SSH warnings from remote repositories broke the "spot" check
|
|
||||||
and other actions as well.
|
|
||||||
* #1295: Fix the ZFS hook to properly unmount snapshots for empty datasets.
|
|
||||||
|
|
||||||
2.1.4
|
|
||||||
* #1266: Add a stand-alone borgmatic Linux binary to the release downloads to serve as another way
|
|
||||||
to install borgmatic. Consider this binary a beta feature.
|
|
||||||
* #1286: Fix a regression in which running borgmatic with no arguments and no configuration files
|
|
||||||
doesn't error as expected.
|
|
||||||
* #1257: Fix for the Loki monitoring hook not respecting the monitoring verbosity.
|
|
||||||
* #1264: Improve performance of the "info" and "repo-list" actions by eliminating a second "borg
|
|
||||||
info" call that supports a "no matching archives" warning. The warning still occurs; it's just
|
|
||||||
done now without the extra "borg info" call.
|
|
||||||
* #1268: Fix the "spot" check, "extract" check, and all uses of the archive name "latest" to
|
|
||||||
respect the "match_archives" and "archives_name_format" options. This means that borgmatic now
|
|
||||||
uses the "latest" archive that also matches those options instead of the latest archive overall.
|
|
||||||
* When Borg exits with a warning exit code, show a description of it, so you don't have to look up
|
|
||||||
the code.
|
|
||||||
* Split out borgmatic installation documentation to its own page, so it's easier to find.
|
|
||||||
* Switch the default borgmatic installation method from pipx to uv, as uv is faster and used for
|
|
||||||
borgmatic development. If you'd like to switch, see the documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/how-to/upgrade/
|
|
||||||
* Move the project tracker from Gitea to Forgejo.
|
|
||||||
* Fix a regression in which borgmatic didn't show an error message when run with no configuration.
|
|
||||||
* Fix a traceback in the "spot" check with Borg 2.
|
|
||||||
|
|
||||||
2.1.3
|
|
||||||
* #1175: Add a "files_changed" option for customizing Borg's file modification detection.
|
|
||||||
* #1175: Add a "msgpack_version_check" option to prevent Borg from validating msgpack's version.
|
|
||||||
* #1218: Add a "config show" action to display computed borgmatic configuration as YAML or JSON,
|
|
||||||
handy for fetching borgmatic configuration from external scripts. See the documentation for more
|
|
||||||
information:
|
|
||||||
https://torsion.org/borgmatic/reference/command-line/actions/config-show/
|
|
||||||
* #1228: Adjust the "spot" check so error output includes more information about what failed.
|
|
||||||
* #1236: Fix the "spot" check to skip hard links, as Borg doesn't produces hashes for them.
|
|
||||||
* #1243: Add a "diff" action for viewing the difference between the contents of two archives.
|
|
||||||
* #1248: Go back to treating Borg "file not found" warnings (exit code 107) as
|
|
||||||
warnings instead of errors. Otherwise, borgmatic can error on files that a user intentionally
|
|
||||||
deletes while a backup is running. You can still override this behavior with the
|
|
||||||
"borg_exit_codes" option. See the documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/how-to/customize-warnings-and-errors/
|
|
||||||
* #1248: Un-deprecate the "source_directories_must_exist" option and default it to true, to
|
|
||||||
compensate for Borg "file not found" warnings no longer being treated as errors.
|
|
||||||
* #1269: Fix the ZFS hook to support datasets with a "canmount" property of "noauto".
|
|
||||||
* #1270: Follow symlinks when backing up borgmatic configuration files to support the "bootstrap"
|
|
||||||
action.
|
|
||||||
* #1274: Add an optional override for the documentation development listen port and use Podman
|
|
||||||
Compose if present.
|
|
||||||
* #1281: Fix a unicode error when backing up a non-UTF-8 source filename with a
|
|
||||||
corresponding system locale.
|
|
||||||
* Add a policy about the use of generative AI in the borgmatic codebase:
|
|
||||||
https://torsion.org/borgmatic/how-to/develop-on-borgmatic/#use-of-generative-ai
|
|
||||||
|
|
||||||
2.1.2
|
|
||||||
* #1231: If a source file is deleted during a "spot" check, consider the file as non-matching
|
|
||||||
and move on instead of immediately failing the entire check.
|
|
||||||
* #1250: Fix a regression in which the "--stats" flag hides statistics at default verbosity.
|
|
||||||
* #1251: Fix a regression in the ntfy monitoring hook in which borgmatic sends tags incorrectly,
|
|
||||||
resulting in "400 Bad Request" from ntfy.
|
|
||||||
* #1258: Fix a "codec can't decode byte" error when running commands that output multi-byte unicode
|
|
||||||
characters.
|
|
||||||
* #1260: Fix for SSH warnings from Borg showing up as JSON logs even without the "--log-json" flag.
|
|
||||||
* #1252: Work around Borg returning a warning exit code when a repository/archive check fails. Now,
|
|
||||||
borgmatic interprets such failures as errors.
|
|
||||||
* Deduplicate overlapping source directories and patterns so they don't throw off "spot" check file
|
|
||||||
counts and cause spurious check failures.
|
|
||||||
|
|
||||||
2.1.1
|
|
||||||
* #1241: For the "recreate" action, actually pass the "--dry-run" flag through to Borg instead of
|
|
||||||
just skipping the Borg call.
|
|
||||||
* #1242: Fix a regression in which the "spot" check hung while collecting archive contents.
|
|
||||||
* #1244: When the "unsafe_skip_path_validation_before_create" option is enabled, don't log a
|
|
||||||
warning about it.
|
|
||||||
* #1245: Fix a regression in which the KeePassXC credential hook password prompt was invisible.
|
|
||||||
* #1246: Fix a regression in which the ntfy monitoring hook failed to send a ping when the
|
|
||||||
"priority" option was set.
|
|
||||||
|
|
||||||
2.1.0
|
|
||||||
* TL;DR: Many logging, memory, and performance improvements. Mind those breaking changes!
|
|
||||||
* #485: When running commands (database clients, command hooks, etc.), elevate stderr output to
|
|
||||||
show up as borgmatic error logs.
|
|
||||||
* #858: With the "--log-json" flag, log borgmatic's own logs as JSON, not just Borg's.
|
|
||||||
* #1092: BREAKING: Treat most Borg warnings as errors by default, so for instance backups now fail
|
|
||||||
when source directories are missing. You can still override this behavior with the
|
|
||||||
"borg_exit_codes" option. See the documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/how-to/customize-warnings-and-errors/
|
|
||||||
* #1092: Deprecate the "source_directories_must_exist" option, as borgmatic now treats "backup file
|
|
||||||
not found" warnings from Borg as errors, which accomplishes the same thing.
|
|
||||||
* #1132: BREAKING/SECURITY: For the Healthchecks, Apprise, Pagerduty, and Loki monitoring hooks,
|
|
||||||
disable log sending when not explicitly enabled. This avoids revealing private log information to
|
|
||||||
third-party services. To send logs anyway, set the monitoring hook's "send_logs" option to
|
|
||||||
"true".
|
|
||||||
* #1204: When verbosity levels differ between console/monitoring/syslog/file, log Borg's output to
|
|
||||||
each one at a different level. Previously, it was logged at the maximum level of all the
|
|
||||||
verbosities.
|
|
||||||
* #1208: Fix for the "restore" action incorrectly extracting more database dumps than the
|
|
||||||
"--database" flag specifies.
|
|
||||||
* #1210: Fix an error when running the "spot" check or "extract" action with the "progress" option
|
|
||||||
or "--progress" flag.
|
|
||||||
* #1211: Fix an error about the runtime directory getting excluded by tweaking its logic and
|
|
||||||
lowering the error to a warning.
|
|
||||||
* #1212: Fix an error when restoring multiple directory-format database dumps at once.
|
|
||||||
* #1213: BREAKING: Support disabling both constant and variable interpolation by escaping with
|
|
||||||
backslashes. For instance, interpret "\{name\}" as literally "{name}" instead of trying to
|
|
||||||
resolve it as a constant/variable.
|
|
||||||
* #1220: Cleanup snapshots immediately after ZFS, LVM, or Btrfs hooks error—rather than waiting
|
|
||||||
until the next time borgmatic runs.
|
|
||||||
* #1221: Add an "unsafe_skip_path_validation_before_create" option to skip pre-backup safety
|
|
||||||
validation so as to reduce backup times on large filesystems.
|
|
||||||
* #1224: When running an "extract" check with the "--progress" flag, show file extraction progress.
|
|
||||||
* #1225: Improve performance and greatly reduce memory usage during pre-backup safety validation on
|
|
||||||
large filesystems.
|
|
||||||
* #1230: Fix another warning from LVM about leaked file descriptors, this time when calling
|
|
||||||
"lvcreate" from a command hook.
|
|
||||||
* #1234: Fix for the ntfy monitoring hook erroring on emojis in the "title" option.
|
|
||||||
* When syslog verbosity is enabled, log to systemd's journal (if present) with
|
|
||||||
structured data. See the documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/reference/command-line/logging/#systemd-journal
|
|
||||||
* SECURITY: Prevent shell injection attacks via constant interpolation in command hooks. (This was
|
|
||||||
already implemented for deprecated "before_*"/"after_*" command hooks.)
|
|
||||||
* Fix for an error in the "key import" action when importing a key from stdin.
|
|
||||||
* Fix the "recreate" action to include borgmatic-specific paths (database dumps, etc.) in recreated
|
|
||||||
archives.
|
|
||||||
* Update the "list" action to support the "--json" flag when the "--archive" flag is also used.
|
|
||||||
* Promote the ZFS, LVM, and Btrfs hooks from beta features to stable.
|
|
||||||
|
|
||||||
2.0.13
|
|
||||||
* #1054: Allow the Btrfs hook to create and delete snapshots even when running
|
|
||||||
as a non-root user. See the documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/reference/configuration/data-sources/btrfs/#non-root-user
|
|
||||||
* #1179: Add a "file_list_format" option for setting the "list" action's output format and an
|
|
||||||
"archive_list_format" option for setting the "repo-list" action's format.
|
|
||||||
* #1192: Fix for over-aggressive deduplication of source directories that contain the borgmatic
|
|
||||||
runtime directory, potentially resulting in data loss (data not getting backed up) when
|
|
||||||
snapshotting these source directories.
|
|
||||||
* #1192, #1163: Document potential interactions between security settings in borgmatic's sample
|
|
||||||
systemd service file and the ZFS, LVM, and Btrfs hooks.
|
|
||||||
* #1193: In the documentation for the MariaDB/MySQL database hooks, clarify how to set custom
|
|
||||||
command-line flags for database commands.
|
|
||||||
* #1193: For the MariaDB and MySQL database hooks, add a "socket_path" option for Unix socket
|
|
||||||
database connections.
|
|
||||||
* #1193: For the MariaDB and MySQL database hooks, creates a consistent snapshot by dumping all
|
|
||||||
tables in a single transaction.
|
|
||||||
* #1194: Fix for an incorrect diff command shown when running the "generate config" action with a
|
|
||||||
source configuration file.
|
|
||||||
* #1195: Fix a regression in the ZFS, LVM, and Btrfs hooks in which snapshotted paths ignored
|
|
||||||
global excludes.
|
|
||||||
* #1201: Document a problematic interaction between borgmatic and systemd-tmpfiles:
|
|
||||||
https://torsion.org/borgmatic/reference/configuration/runtime-directory/#systemd-tmpfiles
|
|
||||||
* #1203: Fix that errors and exits when the borgmatic runtime directory is partially excluded by
|
|
||||||
configured excludes. Previously, borgmatic only errored when the runtime directory was completely
|
|
||||||
excluded.
|
|
||||||
* #1206: Adjust Btrfs snapshot paths so that Borg 1.x gets file cache hits when backing them up,
|
|
||||||
improving performance.
|
|
||||||
* Update the sample systemd timer with a shorter random delay when catching up on a missed run.
|
|
||||||
|
|
||||||
2.0.12
|
|
||||||
* #1127: Fix for the database hooks not respecting the "working_directory" option.
|
|
||||||
* #1181: Add an "ask_for_password" option to the KeePassXC credential hook for disabling
|
|
||||||
KeePassXC's password prompt, e.g. if you're only using a key file to decrypt your database.
|
|
||||||
* #1184: Fix the fish shell completion's detection of version mismatches.
|
|
||||||
* #1186: Fix a regression in the Btrfs hook in which subvolume snapshots didn't get cleaned up
|
|
||||||
until the start of the next borgmatic run.
|
|
||||||
* In the SQLite database hook, run SQLite such that it exits upon encountering an error instead of,
|
|
||||||
you know, not doing that.
|
|
||||||
* Add documentation on repositories, including SSH, Rclone, S3, and B2:
|
|
||||||
https://torsion.org/borgmatic/reference/configuration/repositories/
|
|
||||||
* Improve documentation search results for individual configuration options.
|
|
||||||
* Add borgmatic release artifacts (wheel and tarball) to each release on the releases page:
|
|
||||||
https://projects.torsion.org/borgmatic-collective/borgmatic/releases
|
|
||||||
* Move the tarball of borgmatic's HTML documentation from the packages page to the releases page.
|
|
||||||
|
|
||||||
2.0.11
|
|
||||||
* #957: Document borgmatic's limitations around parallelism—both its own and Borg's. See the
|
|
||||||
documentation for more information:
|
|
||||||
https://torsion.org/borgmatic/how-to/make-per-application-backups/#limitations
|
|
||||||
* #1165: Fix for when the systemd service directories (RuntimeDirectory and StateDirectory) each
|
|
||||||
contain multiple paths.
|
|
||||||
* #1168: Fix for the "list", "info", and "delete" options in "extra_borg_options" being ignored
|
|
||||||
when "--archive" is omitted with Borg 1.x.
|
|
||||||
* #1169: Fix for a regression in the ZFS, LVM, and Btrfs hooks in which partial excludes of
|
|
||||||
snapshot paths were ignored.
|
|
||||||
* #1170: Fix for an inconsistent log level for Borg's last output line before exiting.
|
|
||||||
* #1172: Add an "environment" option to the Sentry monitoring hook.
|
|
||||||
* #1176: Fix the "--repository" flag not applying to command hooks.
|
|
||||||
* Add a "rename" option to "extra_borg_options" to support passing arbitrary flags to "borg
|
|
||||||
rename".
|
|
||||||
* Add documentation on patterns and excludes:
|
|
||||||
https://torsion.org/borgmatic/reference/configuration/patterns-and-excludes/
|
|
||||||
* Drop support for Python 3.9, which has been end-of-lifed.
|
|
||||||
|
|
||||||
2.0.10
|
|
||||||
* #427: Expand the "extra_borg_options" option to support passing arbitrary Borg flags to every
|
|
||||||
Borg sub-command that borgmatic uses. As part of this, deprecate the "init" option under
|
|
||||||
"borg_extra_options" in favor of "repo_create".
|
|
||||||
* #942: Factor reference material out of the documentation how-to guides. This means there's now a
|
|
||||||
whole reference section in the docs! Check it out: https://torsion.org/borgmatic/
|
|
||||||
* #973: For the MariaDB and MySQL database hooks, add a "skip_names" option to ignore particular
|
|
||||||
databases when dumping "all".
|
|
||||||
* #1150: Fix for a runtime directory error when the "create" action is used with the "--log-json"
|
|
||||||
flag.
|
|
||||||
* #1150: Fix for a runtime directory error when the configured patterns contain a global exclude.
|
|
||||||
* #1161: Fix a traceback (TypeError) in the "check" action with Python 3.14.
|
|
||||||
* #1166: Add a "borg_key_file" option for setting the Borg repository key file path.
|
|
||||||
* Add documentation search.
|
|
||||||
* Change the URL of the local documentation development server to be more like the production URL.
|
|
||||||
|
|
||||||
2.0.9
|
|
||||||
* #1105: More accurately collect Btrfs subvolumes to snapshot. As part of this, the Btrfs hook no
|
|
||||||
longer uses "findmnt" and the "findmnt_command" option is deprecated.
|
|
||||||
* #1123: Add loading of systemd credentials even when running borgmatic outside of a systemd
|
* #1123: Add loading of systemd credentials even when running borgmatic outside of a systemd
|
||||||
service.
|
service.
|
||||||
|
* #1134: Add a "steps" option to run command hooks around particular sub-action steps like
|
||||||
|
individual checks.
|
||||||
* #1149: Add support for Python 3.14.
|
* #1149: Add support for Python 3.14.
|
||||||
* #1149: Include automated tests in the source dist tarball uploaded to PyPI.
|
* #1149: Include automated tests in the source dist tarball uploaded to PyPI.
|
||||||
* #1151: Fix snapshotting in the ZFS, Btrfs, and LVM hooks to play nicely with the Borg 1.4+
|
* #1151: Fix snapshotting in the ZFS, Btrfs, and LVM hooks to play nicely with the Borg 1.4+
|
||||||
"slashdot" hack within source directory paths.
|
"slashdot" hack within source directory paths.
|
||||||
* #1152: Fix a regression in the Loki monitoring hook in which log messages weren't sending.
|
|
||||||
* #1156: Fix snapshotting in the ZFS, Btrfs, and LVM hooks to snapshot both parent and child
|
|
||||||
volumes/filesystems instead of just the parent. As part of this fix, borgmatic no longer
|
|
||||||
deduplicates patterns except for those containing the borgmatic runtime directory.
|
|
||||||
* Fix a traceback (TypeError) regression in the "spot" check when the "local_path" option isn't
|
|
||||||
set.
|
|
||||||
|
|
||||||
2.0.8
|
2.0.8
|
||||||
* #1114: Document systemd configuration changes for the ZFS filesystem hook.
|
* #1114: Document systemd configuration changes for the ZFS filesystem hook.
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
title: borgmatic
|
title: borgmatic
|
||||||
permalink: index.html
|
permalink: index.html
|
||||||
---
|
---
|
||||||
<img src="https://torsion.org/borgmatic/static/borgmatic.png" alt="borgmatic logo" width="150px" style="float: right; padding-left: 1em;">
|
|
||||||
|
## It's your data. Keep it that way.
|
||||||
|
|
||||||
|
<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
|
borgmatic is simple, configuration-driven backup software for servers and
|
||||||
workstations. Protect your files with client-side encryption. Backup your
|
workstations. Protect your files with client-side encryption. Backup your
|
||||||
@@ -57,48 +60,47 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
|||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
||||||
<a href="https://www.postgresql.org/"><img src="https://torsion.org/borgmatic/static/postgresql.png" alt="PostgreSQL" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<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="https://torsion.org/borgmatic/static/mysql.png" alt="MySQL" 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="https://torsion.org/borgmatic/static/mariadb.png" alt="MariaDB" 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="https://torsion.org/borgmatic/static/mongodb.png" alt="MongoDB" 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="https://torsion.org/borgmatic/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://openzfs.org/"><img src="https://torsion.org/borgmatic/static/openzfs.png" alt="OpenZFS" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://openzfs.org/"><img src="docs/static/openzfs.png" alt="OpenZFS" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://btrfs.readthedocs.io/"><img src="https://torsion.org/borgmatic/static/btrfs.png" alt="Btrfs" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://btrfs.readthedocs.io/"><img src="docs/static/btrfs.png" alt="Btrfs" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://sourceware.org/lvm2/"><img src="https://torsion.org/borgmatic/static/lvm.png" alt="LVM" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://sourceware.org/lvm2/"><img src="docs/static/lvm.png" alt="LVM" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://rclone.org"><img src="https://torsion.org/borgmatic/static/rclone.png" alt="rclone" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://rclone.org"><img src="docs/static/rclone.png" alt="rclone" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="https://torsion.org/borgmatic/static/borgbase.png" alt="BorgBase" 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>
|
||||||
|
|
||||||
|
|
||||||
### Monitoring
|
### Monitoring
|
||||||
|
|
||||||
<a href="https://healthchecks.io/"><img src="https://torsion.org/borgmatic/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://uptime.kuma.pet/"><img src="https://torsion.org/borgmatic/static/uptimekuma.png" alt="Uptime Kuma" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://uptime.kuma.pet/"><img src="docs/static/uptimekuma.png" alt="Uptime Kuma" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://cronitor.io/"><img src="https://torsion.org/borgmatic/static/cronitor.png" alt="Cronitor" 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="https://torsion.org/borgmatic/static/cronhub.png" alt="Cronhub" 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="https://torsion.org/borgmatic/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://www.pushover.net/"><img src="https://torsion.org/borgmatic/static/pushover.png" alt="Pushover" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://www.pushover.net/"><img src="docs/static/pushover.png" alt="Pushover" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://ntfy.sh/"><img src="https://torsion.org/borgmatic/static/ntfy.png" alt="ntfy" 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="https://torsion.org/borgmatic/static/loki.png" alt="Loki" 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="https://torsion.org/borgmatic/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://www.zabbix.com/"><img src="https://torsion.org/borgmatic/static/zabbix.png" alt="Zabbix" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://www.zabbix.com/"><img src="docs/static/zabbix.png" alt="Zabbix" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://sentry.io/"><img src="https://torsion.org/borgmatic/static/sentry.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://sentry.io/"><img src="docs/static/sentry.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
|
|
||||||
|
|
||||||
### Credentials
|
### Credentials
|
||||||
|
|
||||||
<a href="https://systemd.io/"><img src="https://torsion.org/borgmatic/static/systemd.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://systemd.io/"><img src="docs/static/systemd.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://www.docker.com/"><img src="https://torsion.org/borgmatic/static/docker.png" alt="Docker" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://www.docker.com/"><img src="docs/static/docker.png" alt="Docker" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://podman.io/"><img src="https://torsion.org/borgmatic/static/podman.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://podman.io/"><img src="docs/static/podman.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
<a href="https://keepassxc.org/"><img src="https://torsion.org/borgmatic/static/keepassxc.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
<a href="https://keepassxc.org/"><img src="docs/static/keepassxc.png" alt="Podman" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||||
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
Your first steps are to
|
Your first step is to [install and configure
|
||||||
[install](https://torsion.org/borgmatic/how-to/install-borgmatic/) and
|
borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/).
|
||||||
[configure borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/).
|
|
||||||
|
|
||||||
For additional documentation, check out the links on the top/left
|
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
|
for <a href="https://torsion.org/borgmatic/#documentation">borgmatic how-to and
|
||||||
reference guides</a>.
|
reference guides</a>.
|
||||||
|
|
||||||
@@ -130,7 +132,7 @@ first. If you prefer to use an existing GitHub account, you can skip account
|
|||||||
creation and [login directly](https://projects.torsion.org/user/login).
|
creation and [login directly](https://projects.torsion.org/user/login).
|
||||||
|
|
||||||
Also see the [security
|
Also see the [security
|
||||||
policy](https://torsion.org/borgmatic/security-policy/) for any security
|
policy](https://torsion.org/borgmatic/docs/security-policy/) for any security
|
||||||
issues.
|
issues.
|
||||||
|
|
||||||
|
|
||||||
@@ -175,7 +177,7 @@ discuss your idea. Note that you'll need to
|
|||||||
first. In general, contributions are very welcome. We don't bite!
|
first. In general, contributions are very welcome. We don't bite!
|
||||||
|
|
||||||
Also, please check out the [borgmatic development
|
Also, please check out the [borgmatic development
|
||||||
how-to](https://torsion.org/borgmatic/how-to/develop-on-borgmatic/) for
|
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
||||||
info on cloning source code, running tests, etc.
|
info on cloning source code, running tests, etc.
|
||||||
|
|
||||||
### Recent contributors
|
### Recent contributors
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
.
|
|
||||||
apprise
|
|
||||||
attrs
|
|
||||||
certifi
|
|
||||||
charset-normalizer
|
|
||||||
click
|
|
||||||
idna
|
|
||||||
jsonschema
|
|
||||||
jsonschema-specifications
|
|
||||||
markdown
|
|
||||||
oauthlib
|
|
||||||
packaging
|
|
||||||
pyyaml
|
|
||||||
referencing
|
|
||||||
requests
|
|
||||||
requests-oauthlib
|
|
||||||
rpds-py
|
|
||||||
ruamel-yaml
|
|
||||||
urllib3
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# This file was autogenerated by uv via the following command:
|
|
||||||
# uv pip compile --annotation-style line binary_requirements.in -o binary_requirements.txt
|
|
||||||
apprise==1.10.0 # via -r binary_requirements.in
|
|
||||||
attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.in
|
|
||||||
. # via -r binary_requirements.in
|
|
||||||
certifi==2026.4.22 # via apprise, requests, -r binary_requirements.in
|
|
||||||
charset-normalizer==3.4.7 # via requests, -r binary_requirements.in
|
|
||||||
click==8.3.3 # via apprise, -r binary_requirements.in
|
|
||||||
idna==3.13 # via requests, -r binary_requirements.in
|
|
||||||
jsonschema==4.26.0 # via borgmatic, -r binary_requirements.in
|
|
||||||
jsonschema-specifications==2025.9.1 # via jsonschema, -r binary_requirements.in
|
|
||||||
markdown==3.10.2 # via apprise, -r binary_requirements.in
|
|
||||||
oauthlib==3.3.1 # via requests-oauthlib, -r binary_requirements.in
|
|
||||||
packaging==26.2 # via borgmatic, -r binary_requirements.in
|
|
||||||
pyyaml==6.0.3 # via apprise, -r binary_requirements.in
|
|
||||||
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r binary_requirements.in
|
|
||||||
requests==2.33.1 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
|
|
||||||
requests-oauthlib==2.0.0 # via apprise, -r binary_requirements.in
|
|
||||||
rpds-py==0.30.0 # via jsonschema, referencing, -r binary_requirements.in
|
|
||||||
ruamel-yaml==0.19.1 # via borgmatic, -r binary_requirements.in
|
|
||||||
urllib3==2.6.3 # via requests, -r binary_requirements.in
|
|
||||||
+24
-19
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.borg.borg
|
import borgmatic.borg.borg
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,22 +19,26 @@ def run_borg(
|
|||||||
'''
|
'''
|
||||||
Run the "borg" action for the given repository.
|
Run the "borg" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.info('Running arbitrary Borg command')
|
if borg_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
repository,
|
||||||
repository['path'],
|
borg_arguments.repository,
|
||||||
borg_arguments.archive,
|
):
|
||||||
config,
|
logger.info('Running arbitrary Borg command')
|
||||||
local_borg_version,
|
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
global_arguments,
|
repository['path'],
|
||||||
local_path,
|
borg_arguments.archive,
|
||||||
remote_path,
|
config,
|
||||||
)
|
local_borg_version,
|
||||||
borgmatic.borg.borg.run_arbitrary_borg(
|
global_arguments,
|
||||||
repository['path'],
|
local_path,
|
||||||
config,
|
remote_path,
|
||||||
local_borg_version,
|
)
|
||||||
options=borg_arguments.options,
|
borgmatic.borg.borg.run_arbitrary_borg(
|
||||||
archive=archive_name,
|
repository['path'],
|
||||||
local_path=local_path,
|
config,
|
||||||
remote_path=remote_path,
|
local_borg_version,
|
||||||
)
|
options=borg_arguments.options,
|
||||||
|
archive=archive_name,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import borgmatic.borg.break_lock
|
import borgmatic.borg.break_lock
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,12 +18,16 @@ def run_break_lock(
|
|||||||
'''
|
'''
|
||||||
Run the "break-lock" action for the given repository.
|
Run the "break-lock" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.info('Breaking repository and cache locks')
|
if break_lock_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
borgmatic.borg.break_lock.break_lock(
|
repository,
|
||||||
repository['path'],
|
break_lock_arguments.repository,
|
||||||
config,
|
):
|
||||||
local_borg_version,
|
logger.info('Breaking repository and cache locks')
|
||||||
global_arguments,
|
borgmatic.borg.break_lock.break_lock(
|
||||||
local_path=local_path,
|
repository['path'],
|
||||||
remote_path=remote_path,
|
config,
|
||||||
)
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import borgmatic.borg.change_passphrase
|
import borgmatic.borg.change_passphrase
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,13 +18,20 @@ def run_change_passphrase(
|
|||||||
'''
|
'''
|
||||||
Run the "key change-passphrase" action for the given repository.
|
Run the "key change-passphrase" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.info('Changing repository passphrase')
|
if (
|
||||||
borgmatic.borg.change_passphrase.change_passphrase(
|
change_passphrase_arguments.repository is None
|
||||||
repository['path'],
|
or borgmatic.config.validate.repositories_match(
|
||||||
config,
|
repository,
|
||||||
local_borg_version,
|
change_passphrase_arguments.repository,
|
||||||
change_passphrase_arguments,
|
)
|
||||||
global_arguments,
|
):
|
||||||
local_path=local_path,
|
logger.info('Changing repository passphrase')
|
||||||
remote_path=remote_path,
|
borgmatic.borg.change_passphrase.change_passphrase(
|
||||||
)
|
repository['path'],
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
change_passphrase_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
|||||||
+143
-145
@@ -9,7 +9,6 @@ import pathlib
|
|||||||
import random
|
import random
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
import borgmatic.actions.config.bootstrap
|
import borgmatic.actions.config.bootstrap
|
||||||
@@ -21,8 +20,11 @@ import borgmatic.borg.extract
|
|||||||
import borgmatic.borg.list
|
import borgmatic.borg.list
|
||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.borg.state
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
|
import borgmatic.config.validate
|
||||||
import borgmatic.execute
|
import borgmatic.execute
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
DEFAULT_CHECKS = (
|
DEFAULT_CHECKS = (
|
||||||
{'name': 'repository', 'frequency': '1 month'},
|
{'name': 'repository', 'frequency': '1 month'},
|
||||||
@@ -244,7 +246,7 @@ def write_check_time(path): # pragma: no cover
|
|||||||
logger.debug(f'Writing check time at {path}')
|
logger.debug(f'Writing check time at {path}')
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
|
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
|
||||||
pathlib.Path(path).touch(mode=0o600)
|
pathlib.Path(path, mode=0o600).touch()
|
||||||
|
|
||||||
|
|
||||||
def read_check_time(path):
|
def read_check_time(path):
|
||||||
@@ -381,9 +383,12 @@ def collect_spot_check_source_paths(
|
|||||||
dry_run=True,
|
dry_run=True,
|
||||||
repository_path=repository['path'],
|
repository_path=repository['path'],
|
||||||
# Omit "progress" because it interferes with "list_details".
|
# Omit "progress" because it interferes with "list_details".
|
||||||
config=dict(config, progress=False, list_details=True),
|
config=dict(
|
||||||
|
{option: value for option, value in config.items() if option != 'progress'},
|
||||||
|
list_details=True,
|
||||||
|
),
|
||||||
patterns=borgmatic.actions.pattern.process_patterns(
|
patterns=borgmatic.actions.pattern.process_patterns(
|
||||||
borgmatic.actions.pattern.collect_patterns(config, working_directory)
|
borgmatic.actions.pattern.collect_patterns(config)
|
||||||
+ tuple(
|
+ tuple(
|
||||||
borgmatic.borg.pattern.Pattern(
|
borgmatic.borg.pattern.Pattern(
|
||||||
config_path,
|
config_path,
|
||||||
@@ -403,7 +408,7 @@ def collect_spot_check_source_paths(
|
|||||||
)
|
)
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
|
|
||||||
path_lines = borgmatic.execute.execute_command_and_capture_output(
|
paths_output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
create_flags + create_positional_arguments,
|
create_flags + create_positional_arguments,
|
||||||
capture_stderr=True,
|
capture_stderr=True,
|
||||||
environment=borgmatic.borg.environment.make_environment(config),
|
environment=borgmatic.borg.environment.make_environment(config),
|
||||||
@@ -412,20 +417,14 @@ def collect_spot_check_source_paths(
|
|||||||
borg_exit_codes=config.get('borg_exit_codes'),
|
borg_exit_codes=config.get('borg_exit_codes'),
|
||||||
)
|
)
|
||||||
|
|
||||||
paths = (
|
paths = tuple(
|
||||||
path_line.split(' ', 1)[1]
|
path_line.split(' ', 1)[1]
|
||||||
for path_line in path_lines
|
for path_line in paths_output.splitlines()
|
||||||
if path_line and path_line.startswith(('- ', '+ '))
|
if path_line and path_line.startswith(('- ', '+ '))
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
# Use dict.fromkeys() to deduplicate file paths, which are present in Borg's dry run output
|
path for path in paths if os.path.isfile(os.path.join(working_directory or '', path))
|
||||||
# when there are overlapping source patterns. For instance, if both "/foo" and
|
|
||||||
# "/foo/file.txt" are in configured patterns, then "/foo/file.txt" will show up in Borg's
|
|
||||||
# dry run output twice.
|
|
||||||
dict.fromkeys(
|
|
||||||
path for path in paths if os.path.isfile(os.path.join(working_directory or '', path))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -455,22 +454,24 @@ def collect_spot_check_archive_paths(
|
|||||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
entry['path']
|
path
|
||||||
for entry in borgmatic.borg.list.capture_archive_listing(
|
for line in borgmatic.borg.list.capture_archive_listing(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
archive,
|
archive,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
|
path_format='{type} {path}{NUL}',
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
)
|
)
|
||||||
if entry['type'] not in {BORG_DIRECTORY_FILE_TYPE, BORG_PIPE_FILE_TYPE}
|
for (file_type, path) in (line.split(' ', 1),)
|
||||||
if pathlib.Path('borgmatic') not in pathlib.Path(entry['path']).parents
|
if file_type not in {BORG_DIRECTORY_FILE_TYPE, BORG_PIPE_FILE_TYPE}
|
||||||
|
if pathlib.Path('borgmatic') not in pathlib.Path(path).parents
|
||||||
if pathlib.Path(borgmatic_source_directory.lstrip(os.path.sep))
|
if pathlib.Path(borgmatic_source_directory.lstrip(os.path.sep))
|
||||||
not in pathlib.Path(entry['path']).parents
|
not in pathlib.Path(path).parents
|
||||||
if pathlib.Path(borgmatic_runtime_directory.lstrip(os.path.sep))
|
if pathlib.Path(borgmatic_runtime_directory.lstrip(os.path.sep))
|
||||||
not in pathlib.Path(entry['path']).parents
|
not in pathlib.Path(path).parents
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -525,100 +526,52 @@ def compare_spot_check_hashes(
|
|||||||
source_sample_paths_subset = tuple(
|
source_sample_paths_subset = tuple(
|
||||||
itertools.islice(source_sample_paths_iterator, SAMPLE_PATHS_SUBSET_COUNT),
|
itertools.islice(source_sample_paths_iterator, SAMPLE_PATHS_SUBSET_COUNT),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not source_sample_paths_subset:
|
if not source_sample_paths_subset:
|
||||||
break
|
break
|
||||||
|
|
||||||
hash_paths = tuple(
|
hash_output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
path for path in source_sample_paths_subset if path in hashable_source_sample_path
|
tuple(
|
||||||
|
shlex.quote(part)
|
||||||
|
for part in shlex.split(spot_check_config.get('xxh64sum_command', 'xxh64sum'))
|
||||||
|
)
|
||||||
|
+ tuple(
|
||||||
|
path for path in source_sample_paths_subset if path in hashable_source_sample_path
|
||||||
|
),
|
||||||
|
working_directory=working_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
source_hashes.update(
|
||||||
hash_lines = borgmatic.execute.execute_command_and_capture_output(
|
**dict(
|
||||||
tuple(
|
(reversed(line.split(' ', 1)) for line in hash_output.splitlines()),
|
||||||
shlex.quote(part)
|
# Represent non-existent files as having empty hashes so the comparison below still
|
||||||
for part in shlex.split(spot_check_config.get('xxh64sum_command', 'xxh64sum'))
|
# works. Same thing for filesystem links, since Borg produces empty archive hashes
|
||||||
)
|
# for them.
|
||||||
+ hash_paths,
|
**{
|
||||||
working_directory=working_directory,
|
path: ''
|
||||||
)
|
for path in source_sample_paths_subset
|
||||||
source_hashes.update(
|
if path not in hashable_source_sample_path
|
||||||
**dict(
|
},
|
||||||
zip(
|
),
|
||||||
# xxh64sum rewrites/escapes the paths that it returns alongside its hashes, for
|
)
|
||||||
# instance if they contain special characters. When that happens, they don't
|
|
||||||
# match the original source paths and therefore hash lookups fail. So when
|
|
||||||
# building this lookup dict, use the original unaltered paths we provided as
|
|
||||||
# input to xxh64sum.
|
|
||||||
hash_paths,
|
|
||||||
(
|
|
||||||
# For some reason, xxh64sum prefixes the hash with a backslash if the path
|
|
||||||
# contains a newline. Work around that.
|
|
||||||
line.split(' ', 1)[0].lstrip('\\')
|
|
||||||
for line in hash_lines
|
|
||||||
),
|
|
||||||
),
|
|
||||||
# Represent non-existent files as having empty hashes so the comparison below still
|
|
||||||
# works. Same thing for filesystem links, since Borg produces empty archive hashes
|
|
||||||
# for them.
|
|
||||||
**{
|
|
||||||
path: ''
|
|
||||||
for path in source_sample_paths_subset
|
|
||||||
if path not in hashable_source_sample_path
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
# This can happen if a file we planned to hash gets deleted right before we try to hash
|
|
||||||
# it. Falling back to individual file hashing allows us to find and mark just the
|
|
||||||
# file(s) with problems instead of failing the whole batch.
|
|
||||||
logger.warning(
|
|
||||||
'Bulk source path hashing failed for this batch; falling back to individual file hashing'
|
|
||||||
)
|
|
||||||
|
|
||||||
for hash_path in hash_paths:
|
|
||||||
try:
|
|
||||||
hash_lines = borgmatic.execute.execute_command_and_capture_output(
|
|
||||||
(
|
|
||||||
*(
|
|
||||||
shlex.quote(part)
|
|
||||||
for part in shlex.split(
|
|
||||||
spot_check_config.get('xxh64sum_command', 'xxh64sum')
|
|
||||||
)
|
|
||||||
),
|
|
||||||
hash_path,
|
|
||||||
),
|
|
||||||
working_directory=working_directory,
|
|
||||||
)
|
|
||||||
source_hashes[hash_path] = next(hash_lines).split(' ', 1)[0].lstrip('\\')
|
|
||||||
except (subprocess.CalledProcessError, StopIteration): # noqa: PERF203
|
|
||||||
logger.warning(
|
|
||||||
f'Source path hashing failed for {hash_path}; treating as missing'
|
|
||||||
)
|
|
||||||
source_hashes[hash_path] = ''
|
|
||||||
|
|
||||||
# Get the hash for each file in the archive.
|
# Get the hash for each file in the archive.
|
||||||
for entry in borgmatic.borg.list.capture_archive_listing(
|
archive_hashes.update(
|
||||||
repository['path'],
|
**dict(
|
||||||
archive,
|
reversed(line.split(' ', 1))
|
||||||
config,
|
for line in borgmatic.borg.list.capture_archive_listing(
|
||||||
local_borg_version,
|
repository['path'],
|
||||||
global_arguments,
|
archive,
|
||||||
list_paths=source_sample_paths_subset,
|
config,
|
||||||
path_format='{xxh64}{path}{linktarget}{target}',
|
local_borg_version,
|
||||||
local_path=local_path,
|
global_arguments,
|
||||||
remote_path=remote_path,
|
list_paths=source_sample_paths_subset,
|
||||||
):
|
path_format='{xxh64} {path}{NUL}',
|
||||||
if not entry:
|
local_path=local_path,
|
||||||
continue
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
# Borg can't get hashes of stored hard links. So if this is a hard link path (and not
|
if line
|
||||||
# deemed as the "original" by Borg), then skip hashing of it.
|
),
|
||||||
if entry.get('linktarget') or entry.get('target'):
|
)
|
||||||
source_hashes.pop(os.path.join('/', entry['path']), None)
|
|
||||||
continue
|
|
||||||
|
|
||||||
archive_hashes[entry['path']] = entry['xxh64']
|
|
||||||
|
|
||||||
# Compare the source hashes with the archive hashes to see how many match.
|
# Compare the source hashes with the archive hashes to see how many match.
|
||||||
failing_paths = []
|
failing_paths = []
|
||||||
@@ -655,6 +608,8 @@ def spot_check(
|
|||||||
disk to those stored in the latest archive. If any differences are beyond configured tolerances,
|
disk to those stored in the latest archive. If any differences are beyond configured tolerances,
|
||||||
then the check fails.
|
then the check fails.
|
||||||
'''
|
'''
|
||||||
|
logger.debug('Running spot check')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
spot_check_config = next(
|
spot_check_config = next(
|
||||||
check for check in config.get('checks', ()) if check.get('name') == 'spot'
|
check for check in config.get('checks', ()) if check.get('name') == 'spot'
|
||||||
@@ -717,7 +672,7 @@ def spot_check(
|
|||||||
)
|
)
|
||||||
logger.debug(f'Paths in latest archive but not source paths: {truncated_archive_paths}')
|
logger.debug(f'Paths in latest archive but not source paths: {truncated_archive_paths}')
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Spot check failed; there are no source paths to compare against the archive',
|
'Spot check failed: There are no source paths to compare against the archive',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate the percentage delta between the source paths count and the archive paths count, and
|
# Calculate the percentage delta between the source paths count and the archive paths count, and
|
||||||
@@ -731,13 +686,19 @@ def spot_check(
|
|||||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||||
placeholder=' ...',
|
placeholder=' ...',
|
||||||
)
|
)
|
||||||
|
logger.debug(
|
||||||
|
f'Paths in source paths but not latest archive: {truncated_exclusive_source_paths}',
|
||||||
|
)
|
||||||
truncated_exclusive_archive_paths = textwrap.shorten(
|
truncated_exclusive_archive_paths = textwrap.shorten(
|
||||||
', '.join(set(archive_paths) - rootless_source_paths) or 'none',
|
', '.join(set(archive_paths) - rootless_source_paths) or 'none',
|
||||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||||
placeholder=' ...',
|
placeholder=' ...',
|
||||||
)
|
)
|
||||||
|
logger.debug(
|
||||||
|
f'Paths in latest archive but not source paths: {truncated_exclusive_archive_paths}',
|
||||||
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Spot check failed\n{count_delta_percentage:.2f}% file count delta between source paths ({len(source_paths)} total) and latest archive ({len(archive_paths)} total); tolerance is {spot_check_config["count_tolerance_percentage"]}%\nOnly in source paths: {truncated_exclusive_source_paths}\nOnly in latest archive: {truncated_exclusive_archive_paths}',
|
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(
|
failing_paths = compare_spot_check_hashes(
|
||||||
@@ -762,8 +723,11 @@ def spot_check(
|
|||||||
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
width=MAX_SPOT_CHECK_PATHS_LENGTH,
|
||||||
placeholder=' ...',
|
placeholder=' ...',
|
||||||
)
|
)
|
||||||
|
logger.debug(
|
||||||
|
f'Source paths with data not matching the latest archive: {truncated_failing_paths}',
|
||||||
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Spot check failed\n{failing_percentage:.2f}% of source paths ({len(failing_paths)} out of {len(source_paths)} checked) with data not matching the latest archive; tolerance is {data_tolerance_percentage}%\nSource paths with non-matching data: {truncated_failing_paths}',
|
f'Spot check failed: {failing_percentage:.2f}% of source paths with data not matching the latest archive (tolerance is {data_tolerance_percentage}%)',
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -780,15 +744,22 @@ def run_check(
|
|||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
|
hook_context,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Run the "check" action for the given repository.
|
Run the "check" action for the given repository.
|
||||||
|
|
||||||
Raise ValueError if the Borg repository ID cannot be determined.
|
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
|
||||||
|
|
||||||
logger.info('Running consistency checks')
|
logger.info('Running consistency checks')
|
||||||
|
|
||||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
repository_id = borgmatic.borg.check.get_repository_id(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
@@ -813,48 +784,75 @@ def run_check(
|
|||||||
archives_check_id,
|
archives_check_id,
|
||||||
)
|
)
|
||||||
borg_specific_checks = set(checks).intersection({'repository', 'archives', 'data'})
|
borg_specific_checks = set(checks).intersection({'repository', 'archives', 'data'})
|
||||||
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
|
|
||||||
if borg_specific_checks:
|
if borg_specific_checks:
|
||||||
borgmatic.borg.check.check_archives(
|
with borgmatic.hooks.command.Before_after_hooks(
|
||||||
repository['path'],
|
command_hooks=config.get('commands'),
|
||||||
config,
|
before_after='step',
|
||||||
local_borg_version,
|
umask=config.get('umask'),
|
||||||
check_arguments,
|
working_directory=working_directory,
|
||||||
global_arguments,
|
dry_run=global_arguments.dry_run,
|
||||||
borg_specific_checks,
|
action_names=('check',),
|
||||||
archive_filter_flags,
|
step_names=('archives_repository_data',),
|
||||||
local_path=local_path,
|
**hook_context,
|
||||||
remote_path=remote_path,
|
):
|
||||||
)
|
borgmatic.borg.check.check_archives(
|
||||||
for check in borg_specific_checks:
|
repository['path'],
|
||||||
write_check_time(make_check_time_path(config, repository_id, check, archives_check_id))
|
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:
|
if 'extract' in checks:
|
||||||
logger.info('Running extract check')
|
with borgmatic.hooks.command.Before_after_hooks(
|
||||||
borgmatic.borg.extract.extract_last_archive_dry_run(
|
command_hooks=config.get('commands'),
|
||||||
config,
|
before_after='step',
|
||||||
local_borg_version,
|
umask=config.get('umask'),
|
||||||
global_arguments,
|
working_directory=working_directory,
|
||||||
repository['path'],
|
dry_run=global_arguments.dry_run,
|
||||||
config.get('lock_wait'),
|
action_names=('check',),
|
||||||
local_path,
|
step_names=('extract',),
|
||||||
remote_path,
|
**hook_context,
|
||||||
)
|
):
|
||||||
write_check_time(make_check_time_path(config, repository_id, 'extract'))
|
borgmatic.borg.extract.extract_last_archive_dry_run(
|
||||||
|
|
||||||
if 'spot' in checks:
|
|
||||||
logger.info('Running spot check')
|
|
||||||
with borgmatic.config.paths.Runtime_directory(
|
|
||||||
config, repository_id
|
|
||||||
) as borgmatic_runtime_directory:
|
|
||||||
spot_check(
|
|
||||||
repository,
|
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
|
repository['path'],
|
||||||
|
config.get('lock_wait'),
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
borgmatic_runtime_directory,
|
|
||||||
)
|
)
|
||||||
|
write_check_time(make_check_time_path(config, repository_id, 'extract'))
|
||||||
|
|
||||||
write_check_time(make_check_time_path(config, repository_id, 'spot'))
|
if 'spot' in checks:
|
||||||
|
with borgmatic.hooks.command.Before_after_hooks(
|
||||||
|
command_hooks=config.get('commands'),
|
||||||
|
before_after='step',
|
||||||
|
umask=config.get('umask'),
|
||||||
|
working_directory=working_directory,
|
||||||
|
dry_run=global_arguments.dry_run,
|
||||||
|
action_names=('check',),
|
||||||
|
step_names=('spot',),
|
||||||
|
**hook_context,
|
||||||
|
):
|
||||||
|
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||||
|
spot_check(
|
||||||
|
repository,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
borgmatic_runtime_directory,
|
||||||
|
)
|
||||||
|
|
||||||
|
write_check_time(make_check_time_path(config, repository_id, 'spot'))
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.borg.compact
|
import borgmatic.borg.compact
|
||||||
import borgmatic.borg.feature
|
import borgmatic.borg.feature
|
||||||
|
import borgmatic.config.validate
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,6 +22,12 @@ def run_compact(
|
|||||||
'''
|
'''
|
||||||
Run the "compact" action for the given repository.
|
Run the "compact" action for the given repository.
|
||||||
'''
|
'''
|
||||||
|
if compact_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||||
|
repository,
|
||||||
|
compact_arguments.repository,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
|
if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
|
||||||
logger.info(f'Compacting segments{dry_run_label}')
|
logger.info(f'Compacting segments{dry_run_label}')
|
||||||
borgmatic.borg.compact.compact_segments(
|
borgmatic.borg.compact.compact_segments(
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import borgmatic.borg.extract
|
import borgmatic.borg.extract
|
||||||
import borgmatic.borg.repo_info
|
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
|
import borgmatic.config.validate
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ def load_config_paths_from_archive(
|
|||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path=config.get('local_path', 'borg'),
|
local_path=config.get('local_path'),
|
||||||
remote_path=config.get('remote_path'),
|
remote_path=config.get('remote_path'),
|
||||||
extract_to_stdout=True,
|
extract_to_stdout=True,
|
||||||
)
|
)
|
||||||
@@ -110,18 +111,8 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
|
|||||||
local_path=bootstrap_arguments.local_path,
|
local_path=bootstrap_arguments.local_path,
|
||||||
remote_path=bootstrap_arguments.remote_path,
|
remote_path=bootstrap_arguments.remote_path,
|
||||||
)
|
)
|
||||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
|
||||||
bootstrap_arguments.repository,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path=bootstrap_arguments.local_path,
|
|
||||||
remote_path=bootstrap_arguments.remote_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
with borgmatic.config.paths.Runtime_directory(
|
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||||
config, repository_id
|
|
||||||
) as borgmatic_runtime_directory:
|
|
||||||
manifest_config_paths = load_config_paths_from_archive(
|
manifest_config_paths = load_config_paths_from_archive(
|
||||||
bootstrap_arguments.repository,
|
bootstrap_arguments.repository,
|
||||||
archive_name,
|
archive_name,
|
||||||
@@ -131,13 +122,15 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
|
|||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Bootstrapping configuration paths: {', '.join(manifest_config_paths)}")
|
logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
|
||||||
|
|
||||||
borgmatic.borg.extract.extract_archive(
|
borgmatic.borg.extract.extract_archive(
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
bootstrap_arguments.repository,
|
bootstrap_arguments.repository,
|
||||||
archive_name,
|
archive_name,
|
||||||
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
|
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
|
||||||
|
# Only add progress here and not the extract_archive() call above, because progress
|
||||||
|
# conflicts with extract_to_stdout.
|
||||||
dict(config, progress=bootstrap_arguments.progress or False),
|
dict(config, progress=bootstrap_arguments.progress or False),
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
|
|||||||
@@ -19,33 +19,26 @@ def run_generate(generate_arguments, global_arguments):
|
|||||||
dry_run_label = ' (dry run; not actually writing anything)' if global_arguments.dry_run else ''
|
dry_run_label = ' (dry run; not actually writing anything)' if global_arguments.dry_run else ''
|
||||||
|
|
||||||
logger.answer(
|
logger.answer(
|
||||||
f'Generating configuration files within: {generate_arguments.destination_path}{dry_run_label}'
|
f'Generating a configuration file at: {generate_arguments.destination_filename}{dry_run_label}',
|
||||||
if generate_arguments.split
|
|
||||||
else f'Generating a configuration file at: {generate_arguments.destination_path}{dry_run_label}'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
borgmatic.config.generate.generate_sample_configuration(
|
borgmatic.config.generate.generate_sample_configuration(
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
generate_arguments.source_filename,
|
generate_arguments.source_filename,
|
||||||
generate_arguments.destination_path,
|
generate_arguments.destination_filename,
|
||||||
borgmatic.config.validate.schema_filename(),
|
borgmatic.config.validate.schema_filename(),
|
||||||
overwrite=generate_arguments.overwrite,
|
overwrite=generate_arguments.overwrite,
|
||||||
split=generate_arguments.split,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if generate_arguments.source_filename:
|
if generate_arguments.source_filename:
|
||||||
logger.answer(
|
logger.answer(
|
||||||
f'''
|
f'''
|
||||||
Merged the contents of the source configuration file: {generate_arguments.source_filename}'''
|
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}''',
|
||||||
)
|
)
|
||||||
|
|
||||||
if not generate_arguments.split:
|
|
||||||
logger.answer(
|
|
||||||
f'''To review the changes made, run:
|
|
||||||
|
|
||||||
diff --unified {generate_arguments.source_filename} {generate_arguments.destination_path}''',
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.answer(
|
logger.answer(
|
||||||
'''
|
'''
|
||||||
This includes all available configuration options with example values, the few
|
This includes all available configuration options with example values, the few
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import borgmatic.config.generate
|
|
||||||
import borgmatic.logger
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def run_show(show_arguments, configs):
|
|
||||||
'''
|
|
||||||
Given the show arguments as an argparse.Namespace instance and a dict of configuration filename
|
|
||||||
to corresponding parsed configuration, run the "show" action. That consists of rendering and
|
|
||||||
logging the computed configuration as YAML, separating the configuration for each file with
|
|
||||||
"---".
|
|
||||||
|
|
||||||
If show_arguments.option is set, limit the results to the value of that single option. If
|
|
||||||
show_arguments.json is True, render the results as JSON with one array element per configuration
|
|
||||||
file.
|
|
||||||
'''
|
|
||||||
borgmatic.logger.add_custom_log_levels()
|
|
||||||
|
|
||||||
if show_arguments.json:
|
|
||||||
sys.stdout.write(
|
|
||||||
json.dumps(
|
|
||||||
[
|
|
||||||
config.get(show_arguments.option) if show_arguments.option else config
|
|
||||||
for config in configs.values()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
for config in configs.values():
|
|
||||||
if len(configs) > 1:
|
|
||||||
logger.answer('---')
|
|
||||||
|
|
||||||
logger.answer(
|
|
||||||
borgmatic.config.generate.render_configuration(
|
|
||||||
config.get(show_arguments.option) if show_arguments.option else config
|
|
||||||
).rstrip()
|
|
||||||
)
|
|
||||||
+75
-76
@@ -1,12 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import borgmatic.actions.dump
|
|
||||||
import borgmatic.actions.json
|
import borgmatic.actions.json
|
||||||
import borgmatic.borg.create
|
import borgmatic.borg.create
|
||||||
import borgmatic.borg.feature
|
import borgmatic.borg.feature
|
||||||
import borgmatic.borg.rename
|
import borgmatic.borg.rename
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
|
import borgmatic.config.validate
|
||||||
import borgmatic.hooks.dispatch
|
import borgmatic.hooks.dispatch
|
||||||
from borgmatic.actions import pattern
|
from borgmatic.actions import pattern
|
||||||
|
|
||||||
@@ -30,6 +30,12 @@ def run_create(
|
|||||||
|
|
||||||
If create_arguments.json is True, yield the JSON output from creating the archive.
|
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
|
||||||
|
|
||||||
if config.get('list_details') and config.get('progress'):
|
if config.get('list_details') and config.get('progress'):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'With the create action, only one of --list/--files/list_details and --progress/progress can be used.',
|
'With the create action, only one of --list/--files/list_details and --progress/progress can be used.',
|
||||||
@@ -42,105 +48,98 @@ def run_create(
|
|||||||
|
|
||||||
logger.info(f'Creating archive{dry_run_label}')
|
logger.info(f'Creating archive{dry_run_label}')
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
|
||||||
repository['path'],
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
with borgmatic.config.paths.Runtime_directory(
|
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||||
config, repository_id
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
) as borgmatic_runtime_directory:
|
'remove_data_source_dumps',
|
||||||
|
config,
|
||||||
|
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||||
|
borgmatic_runtime_directory,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
)
|
||||||
patterns = pattern.process_patterns(
|
patterns = pattern.process_patterns(
|
||||||
pattern.collect_patterns(config, working_directory),
|
pattern.collect_patterns(config),
|
||||||
config,
|
config,
|
||||||
working_directory,
|
working_directory,
|
||||||
|
)
|
||||||
|
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
||||||
|
'dump_data_sources',
|
||||||
|
config,
|
||||||
|
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||||
|
config_paths,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
|
patterns,
|
||||||
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
original_patterns = list(patterns)
|
# Process the patterns again in case any data source hooks updated them. Without this step,
|
||||||
|
# we could end up with duplicate paths that cause Borg to hang when it tries to read from
|
||||||
|
# the same named pipe twice.
|
||||||
|
patterns = pattern.process_patterns(
|
||||||
|
patterns,
|
||||||
|
config,
|
||||||
|
working_directory,
|
||||||
|
skip_expand_paths=config_paths,
|
||||||
|
)
|
||||||
|
stream_processes = [process for processes in active_dumps.values() for process in processes]
|
||||||
|
|
||||||
# Use the original patterns so as to disregard any modifications made by any data source
|
# If we have stream processes, we first create an archive with .checkpoint suffix. This is
|
||||||
# hooks, e.g. via dump_data_sources() below.
|
# to make sure we only create a real archive if all the streaming processes completed
|
||||||
with borgmatic.actions.dump.Dump_cleanup(
|
# successfully (create_archive will fail if a streaming process fails, but the archive might
|
||||||
config, borgmatic_runtime_directory, original_patterns, global_arguments.dry_run
|
# have already been created at this point).
|
||||||
):
|
use_checkpoint = bool(stream_processes)
|
||||||
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
|
||||||
'dump_data_sources',
|
|
||||||
config,
|
|
||||||
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
|
||||||
config_paths,
|
|
||||||
borgmatic_runtime_directory,
|
|
||||||
patterns,
|
|
||||||
global_arguments.dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process the patterns again in case any data source hooks updated them. Without this step,
|
json_output = borgmatic.borg.create.create_archive(
|
||||||
# we could end up with duplicate paths that cause Borg to hang when it tries to read from
|
global_arguments.dry_run,
|
||||||
# the same named pipe twice.
|
repository['path'],
|
||||||
patterns = pattern.process_patterns(
|
config,
|
||||||
patterns,
|
patterns,
|
||||||
config,
|
local_borg_version,
|
||||||
working_directory,
|
global_arguments,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
skip_expand_paths=config_paths,
|
archive_suffix='.checkpoint' if use_checkpoint else '',
|
||||||
)
|
local_path=local_path,
|
||||||
stream_processes = [
|
remote_path=remote_path,
|
||||||
process for processes in active_dumps.values() for process in processes
|
json=create_arguments.json,
|
||||||
]
|
comment=create_arguments.comment,
|
||||||
|
stream_processes=stream_processes,
|
||||||
|
)
|
||||||
|
|
||||||
# If we have stream processes, we first create an archive with .checkpoint suffix. This is
|
if use_checkpoint:
|
||||||
# to make sure we only create a real archive if all the streaming processes completed
|
rename_checkpoint_archive(
|
||||||
# successfully (create_archive will fail if a streaming process fails, but the archive might
|
|
||||||
# have already been created at this point).
|
|
||||||
use_checkpoint = bool(stream_processes)
|
|
||||||
|
|
||||||
json_output = borgmatic.borg.create.create_archive(
|
|
||||||
global_arguments.dry_run,
|
|
||||||
repository['path'],
|
repository['path'],
|
||||||
config,
|
|
||||||
patterns,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
global_arguments,
|
||||||
borgmatic_runtime_directory,
|
config,
|
||||||
archive_suffix='.checkpoint' if use_checkpoint else '',
|
local_borg_version,
|
||||||
local_path=local_path,
|
local_path,
|
||||||
remote_path=remote_path,
|
remote_path,
|
||||||
json=create_arguments.json,
|
|
||||||
comment=create_arguments.comment,
|
|
||||||
stream_processes=stream_processes,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if json_output:
|
||||||
|
output = borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||||
if use_checkpoint:
|
if use_checkpoint:
|
||||||
rename_checkpoint_archive(
|
# Patch archive name and ID
|
||||||
|
renamed_archive = borgmatic.borg.repo_list.get_latest_archive(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
global_arguments,
|
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if json_output:
|
output['archive']['name'] = renamed_archive['name']
|
||||||
output = borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
output['archive']['id'] = renamed_archive['id']
|
||||||
if use_checkpoint:
|
|
||||||
# Patch archive name and ID
|
|
||||||
renamed_archive = borgmatic.borg.repo_list.get_latest_archive(
|
|
||||||
repository['path'],
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
output['archive']['name'] = renamed_archive['name']
|
yield output
|
||||||
output['archive']['id'] = renamed_archive['id']
|
|
||||||
|
|
||||||
yield output
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
|
'remove_data_source_dumps',
|
||||||
|
config,
|
||||||
|
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||||
|
borgmatic_runtime_directory,
|
||||||
|
global_arguments.dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def rename_checkpoint_archive(
|
def rename_checkpoint_archive(
|
||||||
|
|||||||
+23
-18
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.actions.arguments
|
import borgmatic.actions.arguments
|
||||||
import borgmatic.borg.delete
|
import borgmatic.borg.delete
|
||||||
|
import borgmatic.borg.repo_delete
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -19,28 +20,32 @@ def run_delete(
|
|||||||
'''
|
'''
|
||||||
Run the "delete" action for the given repository and archive(s).
|
Run the "delete" action for the given repository and archive(s).
|
||||||
'''
|
'''
|
||||||
logger.answer('Deleting archives')
|
if delete_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
|
repository,
|
||||||
|
delete_arguments.repository,
|
||||||
|
):
|
||||||
|
logger.answer('Deleting archives')
|
||||||
|
|
||||||
archive_name = (
|
archive_name = (
|
||||||
borgmatic.borg.repo_list.resolve_archive_name(
|
borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
delete_arguments.archive,
|
delete_arguments.archive,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
)
|
||||||
|
if delete_arguments.archive
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
borgmatic.borg.delete.delete_archives(
|
||||||
|
repository,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
|
borgmatic.actions.arguments.update_arguments(delete_arguments, archive=archive_name),
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
if delete_arguments.archive
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
borgmatic.borg.delete.delete_archives(
|
|
||||||
repository,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
borgmatic.actions.arguments.update_arguments(delete_arguments, archive=archive_name),
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
import borgmatic.actions.pattern
|
|
||||||
import borgmatic.borg.diff
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def run_diff(
|
|
||||||
repository,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
diff_arguments,
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Run the "diff" action for the given repository.
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Only process patterns if only_patterns flag is set
|
|
||||||
if diff_arguments.only_patterns:
|
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
|
||||||
processed_patterns = borgmatic.actions.pattern.process_patterns(
|
|
||||||
(*borgmatic.actions.pattern.collect_patterns(config, working_directory),),
|
|
||||||
config,
|
|
||||||
working_directory,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
processed_patterns = None
|
|
||||||
|
|
||||||
archive = borgmatic.borg.repo_list.resolve_archive_name(
|
|
||||||
repository['path'],
|
|
||||||
diff_arguments.archive,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
)
|
|
||||||
second_archive = borgmatic.borg.repo_list.resolve_archive_name(
|
|
||||||
repository['path'],
|
|
||||||
diff_arguments.second_archive,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
borgmatic.borg.diff.diff(
|
|
||||||
repository['path'],
|
|
||||||
archive,
|
|
||||||
second_archive,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
diff_arguments,
|
|
||||||
global_arguments,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
patterns=processed_patterns,
|
|
||||||
)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import borgmatic.hooks.dispatch
|
|
||||||
|
|
||||||
|
|
||||||
class Dump_cleanup:
|
|
||||||
'''
|
|
||||||
A Python context manager for removing data source dumps before and after wrapped code. Besides
|
|
||||||
doing this for general filesystem cleanliness reasons, leaving old dumps lying around can
|
|
||||||
prevent future dumping from working (in the case of filesystem snapshots) or cause Borg hangs
|
|
||||||
(in the case of database dump named pipes).
|
|
||||||
|
|
||||||
Example use as a context manager:
|
|
||||||
|
|
||||||
with borgmatic.actions.dump.Dump_cleanup(
|
|
||||||
config, borgmatic_runtime_directory, patterns, dry_run,
|
|
||||||
):
|
|
||||||
do_something_like_perform_a_dump_or_restore()
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, config, borgmatic_runtime_directory, patterns, dry_run):
|
|
||||||
'''
|
|
||||||
Given a configuration dict, the borgmatic runtime directory, the configured patterns, and
|
|
||||||
whether this is a dry-run, store these values for use below.
|
|
||||||
'''
|
|
||||||
self.config = config
|
|
||||||
self.borgmatic_runtime_directory = borgmatic_runtime_directory
|
|
||||||
self.patterns = patterns
|
|
||||||
self.dry_run = dry_run
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
'''
|
|
||||||
Remove all data source dumps that exist prior to the wrapped code running.
|
|
||||||
'''
|
|
||||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
|
||||||
'remove_data_source_dumps',
|
|
||||||
self.config,
|
|
||||||
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
|
||||||
self.borgmatic_runtime_directory,
|
|
||||||
self.patterns,
|
|
||||||
self.dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __exit__(self, exception_type, exception, traceback):
|
|
||||||
'''
|
|
||||||
Remove all data source dumps, including any created by the wrapped code.
|
|
||||||
'''
|
|
||||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
|
||||||
'remove_data_source_dumps',
|
|
||||||
self.config,
|
|
||||||
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
|
||||||
self.borgmatic_runtime_directory,
|
|
||||||
self.patterns,
|
|
||||||
self.dry_run,
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import borgmatic.borg.export_key
|
import borgmatic.borg.export_key
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,13 +18,17 @@ def run_export_key(
|
|||||||
'''
|
'''
|
||||||
Run the "key export" action for the given repository.
|
Run the "key export" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.info('Exporting repository key')
|
if export_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
borgmatic.borg.export_key.export_key(
|
repository,
|
||||||
repository['path'],
|
export_arguments.repository,
|
||||||
config,
|
):
|
||||||
local_borg_version,
|
logger.info('Exporting repository key')
|
||||||
export_arguments,
|
borgmatic.borg.export_key.export_key(
|
||||||
global_arguments,
|
repository['path'],
|
||||||
local_path=local_path,
|
config,
|
||||||
remote_path=remote_path,
|
local_borg_version,
|
||||||
)
|
export_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.borg.export_tar
|
import borgmatic.borg.export_tar
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,26 +19,30 @@ def run_export_tar(
|
|||||||
'''
|
'''
|
||||||
Run the "export-tar" action for the given repository.
|
Run the "export-tar" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.info(f'Exporting archive {export_tar_arguments.archive} as tar file')
|
if export_tar_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
borgmatic.borg.export_tar.export_tar_archive(
|
repository,
|
||||||
global_arguments.dry_run,
|
export_tar_arguments.repository,
|
||||||
repository['path'],
|
):
|
||||||
borgmatic.borg.repo_list.resolve_archive_name(
|
logger.info(f'Exporting archive {export_tar_arguments.archive} as tar file')
|
||||||
|
borgmatic.borg.export_tar.export_tar_archive(
|
||||||
|
global_arguments.dry_run,
|
||||||
repository['path'],
|
repository['path'],
|
||||||
export_tar_arguments.archive,
|
borgmatic.borg.repo_list.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,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path=local_path,
|
||||||
remote_path,
|
remote_path=remote_path,
|
||||||
),
|
tar_filter=export_tar_arguments.tar_filter,
|
||||||
export_tar_arguments.paths,
|
strip_components=export_tar_arguments.strip_components,
|
||||||
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,
|
|
||||||
strip_components=export_tar_arguments.strip_components,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.borg.extract
|
import borgmatic.borg.extract
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -19,25 +21,29 @@ def run_extract(
|
|||||||
'''
|
'''
|
||||||
Run the "extract" action for the given repository.
|
Run the "extract" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.info(f'Extracting archive {extract_arguments.archive}')
|
if extract_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
borgmatic.borg.extract.extract_archive(
|
repository,
|
||||||
global_arguments.dry_run,
|
extract_arguments.repository,
|
||||||
repository['path'],
|
):
|
||||||
borgmatic.borg.repo_list.resolve_archive_name(
|
logger.info(f'Extracting archive {extract_arguments.archive}')
|
||||||
|
borgmatic.borg.extract.extract_archive(
|
||||||
|
global_arguments.dry_run,
|
||||||
repository['path'],
|
repository['path'],
|
||||||
extract_arguments.archive,
|
borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
|
repository['path'],
|
||||||
|
extract_arguments.archive,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
),
|
||||||
|
extract_arguments.paths,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path=local_path,
|
||||||
remote_path,
|
remote_path=remote_path,
|
||||||
),
|
destination_path=extract_arguments.destination,
|
||||||
extract_arguments.paths,
|
strip_components=extract_arguments.strip_components,
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import borgmatic.borg.import_key
|
import borgmatic.borg.import_key
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,13 +18,17 @@ def run_import_key(
|
|||||||
'''
|
'''
|
||||||
Run the "key import" action for the given repository.
|
Run the "key import" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.info('Importing repository key')
|
if import_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
borgmatic.borg.import_key.import_key(
|
repository,
|
||||||
repository['path'],
|
import_arguments.repository,
|
||||||
config,
|
):
|
||||||
local_borg_version,
|
logger.info('Importing repository key')
|
||||||
import_arguments,
|
borgmatic.borg.import_key.import_key(
|
||||||
global_arguments,
|
repository['path'],
|
||||||
local_path=local_path,
|
config,
|
||||||
remote_path=remote_path,
|
local_borg_version,
|
||||||
)
|
import_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
|||||||
+27
-22
@@ -4,6 +4,7 @@ import borgmatic.actions.arguments
|
|||||||
import borgmatic.actions.json
|
import borgmatic.actions.json
|
||||||
import borgmatic.borg.info
|
import borgmatic.borg.info
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -22,26 +23,30 @@ def run_info(
|
|||||||
|
|
||||||
If info_arguments.json is True, yield the JSON output from the info for the archive.
|
If info_arguments.json is True, yield the JSON output from the info for the archive.
|
||||||
'''
|
'''
|
||||||
if not info_arguments.json:
|
if info_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
logger.answer('Displaying archive summary information')
|
repository,
|
||||||
|
info_arguments.repository,
|
||||||
|
):
|
||||||
|
if not info_arguments.json:
|
||||||
|
logger.answer('Displaying archive summary information')
|
||||||
|
|
||||||
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
info_arguments.archive,
|
info_arguments.archive,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
json_output = borgmatic.borg.info.display_archives_info(
|
json_output = borgmatic.borg.info.display_archives_info(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
borgmatic.actions.arguments.update_arguments(info_arguments, archive=archive_name),
|
borgmatic.actions.arguments.update_arguments(info_arguments, archive=archive_name),
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
if json_output:
|
if json_output:
|
||||||
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||||
|
|||||||
+30
-25
@@ -3,6 +3,7 @@ import logging
|
|||||||
import borgmatic.actions.arguments
|
import borgmatic.actions.arguments
|
||||||
import borgmatic.actions.json
|
import borgmatic.actions.json
|
||||||
import borgmatic.borg.list
|
import borgmatic.borg.list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -21,29 +22,33 @@ def run_list(
|
|||||||
|
|
||||||
If list_arguments.json is True, yield the JSON output from listing the archive.
|
If list_arguments.json is True, yield the JSON output from listing the archive.
|
||||||
'''
|
'''
|
||||||
if not list_arguments.json:
|
if list_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
if list_arguments.find_paths: # pragma: no cover
|
repository,
|
||||||
logger.answer('Searching archives')
|
list_arguments.repository,
|
||||||
elif not list_arguments.archive: # pragma: no cover
|
):
|
||||||
logger.answer('Listing archives')
|
if not list_arguments.json:
|
||||||
|
if list_arguments.find_paths: # pragma: no cover
|
||||||
|
logger.answer('Searching archives')
|
||||||
|
elif not list_arguments.archive: # pragma: no cover
|
||||||
|
logger.answer('Listing archives')
|
||||||
|
|
||||||
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
list_arguments.archive,
|
list_arguments.archive,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
json_output = borgmatic.borg.list.list_archive(
|
json_output = borgmatic.borg.list.list_archive(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
borgmatic.actions.arguments.update_arguments(list_arguments, archive=archive_name),
|
borgmatic.actions.arguments.update_arguments(list_arguments, archive=archive_name),
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
if json_output:
|
if json_output:
|
||||||
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||||
|
|||||||
+23
-18
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.borg.mount
|
import borgmatic.borg.mount
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,26 +19,30 @@ def run_mount(
|
|||||||
'''
|
'''
|
||||||
Run the "mount" action for the given repository.
|
Run the "mount" action for the given repository.
|
||||||
'''
|
'''
|
||||||
if mount_arguments.archive:
|
if mount_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
logger.info(f'Mounting archive {mount_arguments.archive}')
|
repository,
|
||||||
else: # pragma: nocover
|
mount_arguments.repository,
|
||||||
logger.info('Mounting repository')
|
):
|
||||||
|
if mount_arguments.archive:
|
||||||
|
logger.info(f'Mounting archive {mount_arguments.archive}')
|
||||||
|
else: # pragma: nocover
|
||||||
|
logger.info('Mounting repository')
|
||||||
|
|
||||||
borgmatic.borg.mount.mount_archive(
|
borgmatic.borg.mount.mount_archive(
|
||||||
repository['path'],
|
|
||||||
borgmatic.borg.repo_list.resolve_archive_name(
|
|
||||||
repository['path'],
|
repository['path'],
|
||||||
mount_arguments.archive,
|
borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
|
repository['path'],
|
||||||
|
mount_arguments.archive,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
),
|
||||||
|
mount_arguments,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path=local_path,
|
||||||
remote_path,
|
remote_path=remote_path,
|
||||||
),
|
)
|
||||||
mount_arguments,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -34,11 +34,10 @@ def parse_pattern(pattern_line, default_style=borgmatic.borg.pattern.Pattern_sty
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def collect_patterns(config, working_directory):
|
def collect_patterns(config):
|
||||||
'''
|
'''
|
||||||
Given a configuration dict and the working directory, produce a single sequence of patterns
|
Given a configuration dict, produce a single sequence of patterns comprised of the configured
|
||||||
comprised of the configured source directories, patterns, excludes, pattern files, and exclude
|
source directories, patterns, excludes, pattern files, and exclude files.
|
||||||
files.
|
|
||||||
|
|
||||||
The idea is that Borg has all these different ways of specifying includes, excludes, source
|
The idea is that Borg has all these different ways of specifying includes, excludes, source
|
||||||
directories, etc., but we'd like to collapse them all down to one common format (patterns) for
|
directories, etc., but we'd like to collapse them all down to one common format (patterns) for
|
||||||
@@ -69,8 +68,7 @@ def collect_patterns(config, working_directory):
|
|||||||
+ tuple(
|
+ tuple(
|
||||||
parse_pattern(pattern_line.strip())
|
parse_pattern(pattern_line.strip())
|
||||||
for filename in config.get('patterns_from', ())
|
for filename in config.get('patterns_from', ())
|
||||||
for expanded_path in expand_directory(filename, working_directory)
|
for pattern_line in open(filename, encoding='utf-8').readlines()
|
||||||
for pattern_line in open(expanded_path, encoding='utf-8')
|
|
||||||
if not pattern_line.lstrip().startswith('#')
|
if not pattern_line.lstrip().startswith('#')
|
||||||
if pattern_line.strip()
|
if pattern_line.strip()
|
||||||
)
|
)
|
||||||
@@ -80,8 +78,7 @@ def collect_patterns(config, working_directory):
|
|||||||
borgmatic.borg.pattern.Pattern_style.FNMATCH,
|
borgmatic.borg.pattern.Pattern_style.FNMATCH,
|
||||||
)
|
)
|
||||||
for filename in config.get('exclude_from', ())
|
for filename in config.get('exclude_from', ())
|
||||||
for expanded_path in expand_directory(filename, working_directory)
|
for exclude_line in open(filename, encoding='utf-8').readlines()
|
||||||
for exclude_line in open(expanded_path, encoding='utf-8')
|
|
||||||
if not exclude_line.lstrip().startswith('#')
|
if not exclude_line.lstrip().startswith('#')
|
||||||
if exclude_line.strip()
|
if exclude_line.strip()
|
||||||
)
|
)
|
||||||
@@ -230,69 +227,47 @@ def device_map_patterns(patterns, working_directory=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def deduplicate_runtime_directory_patterns(patterns, config, borgmatic_runtime_directory=None):
|
def deduplicate_patterns(patterns, config):
|
||||||
'''
|
'''
|
||||||
Given a sequence of borgmatic.borg.pattern.Pattern instances, the borgmatic runtime directory,
|
Given a sequence of borgmatic.borg.pattern.Pattern instances and a configuration dict, return
|
||||||
and a configuration dict, return them without any duplicate root child patterns that contain the
|
them with all duplicate root child patterns removed. For instance, if two root patterns are
|
||||||
runtime directory. For instance, if two root patterns are given with paths "/foo" and
|
given with paths "/foo" and "/foo/bar", return just the one with "/foo". Non-root patterns are
|
||||||
"/foo/bar", and the runtime directory is "/foo/bar", return just the "/foo" pattern. Non-root
|
passed through without modification.
|
||||||
patterns and patterns not containing the runtime directory are passed through without
|
|
||||||
modification.
|
|
||||||
|
|
||||||
One exception to deduplication is if two paths are on different filesystems (devices) and
|
The one exception to deduplication is if two paths are on different filesystems (devices) and
|
||||||
"one_file_system" is True in the given configuration. In that case, the paths won't get
|
"one_file_system" is True in the given configuration. In that case, the paths won't get
|
||||||
deduplicated, because Borg won't cross filesystem boundaries when "one_file_system" is True.
|
deduplicated, because Borg won't cross filesystem boundaries when "one_file_system" is True.
|
||||||
|
|
||||||
The idea is that if Borg is given a root parent pattern containing the borgmatic runtime
|
The idea is that if Borg is given a root parent pattern, then it doesn't also need to be given
|
||||||
directory, then Borg doesn't also need to be given child patterns, because it will naturally
|
child patterns, because it will naturally spider the contents of the parent pattern's path. And
|
||||||
spider the contents of the parent pattern's path. Additionally, there are cases where Borg
|
there are cases where Borg coming across the same file twice will result in duplicate reads and
|
||||||
coming across the same file twice will result in duplicate reads and even hangs, e.g. when a
|
even hangs, e.g. when a database hook is using a named pipe for streaming database dumps to
|
||||||
database hook in the borgmatic runtime directory is using a named pipe for streaming database
|
Borg.
|
||||||
dumps to Borg.
|
|
||||||
|
|
||||||
This deduplication is limited to the borgmatic runtime directory (where borgmatic's named pipes
|
|
||||||
exist), because there are other legitimate use cases for parent and child patterns to both exist
|
|
||||||
in patterns. For instance, with some snapshotted filesystems, snapshots don't traverse from a
|
|
||||||
parent filesystem to a child and therefore both need to remain in patterns.
|
|
||||||
|
|
||||||
And for the case of named pipes outside of the borgmatic runtime directory, there is code
|
|
||||||
elsewhere (in the "create" action) that auto-excludes special files to prevent Borg hangs.
|
|
||||||
'''
|
'''
|
||||||
if borgmatic_runtime_directory is None:
|
|
||||||
return patterns
|
|
||||||
|
|
||||||
deduplicated = {} # Use just the keys as an ordered set.
|
deduplicated = {} # Use just the keys as an ordered set.
|
||||||
runtime_directory_parents = set(pathlib.PurePath(borgmatic_runtime_directory).parents).union(
|
|
||||||
{pathlib.PurePath(borgmatic_runtime_directory)}
|
|
||||||
)
|
|
||||||
|
|
||||||
for pattern in patterns:
|
for pattern in patterns:
|
||||||
if pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT:
|
if pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT:
|
||||||
deduplicated[pattern] = True
|
deduplicated[pattern] = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pattern_parents = pathlib.PurePath(pattern.path).parents
|
parents = pathlib.PurePath(pattern.path).parents
|
||||||
|
|
||||||
# If:
|
# If another directory in the given list is a parent of current directory (even n levels up)
|
||||||
#
|
# and both are on the same filesystem (or one_file_system is not set), then the current
|
||||||
# 1. another pattern is a parent of the current pattern (even n levels up),
|
# directory is a duplicate.
|
||||||
# 2. both patterns are parents of the runtime directory (even n levels up),
|
|
||||||
# 3. and both patterns are on the same filesystem (or one_file_system is not set)
|
|
||||||
#
|
|
||||||
# ... then consider the current pattern as a duplicate.
|
|
||||||
for other_pattern in patterns:
|
for other_pattern in patterns:
|
||||||
if other_pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT:
|
if other_pattern.type != borgmatic.borg.pattern.Pattern_type.ROOT:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
device_matches = pattern.device is not None and (
|
if any(
|
||||||
other_pattern.device == pattern.device or config.get('one_file_system') is not True
|
pathlib.PurePath(other_pattern.path) == parent
|
||||||
)
|
and pattern.device is not None
|
||||||
|
and (
|
||||||
if (
|
other_pattern.device == pattern.device
|
||||||
pathlib.PurePath(other_pattern.path) in pattern_parents
|
or config.get('one_file_system') is not True
|
||||||
and pathlib.PurePosixPath(other_pattern.path) in runtime_directory_parents
|
)
|
||||||
and pathlib.PurePosixPath(pattern.path) in runtime_directory_parents
|
for parent in parents
|
||||||
and device_matches
|
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -301,22 +276,16 @@ def deduplicate_runtime_directory_patterns(patterns, config, borgmatic_runtime_d
|
|||||||
return tuple(deduplicated.keys())
|
return tuple(deduplicated.keys())
|
||||||
|
|
||||||
|
|
||||||
def process_patterns(
|
def process_patterns(patterns, config, working_directory, skip_expand_paths=None):
|
||||||
patterns, config, working_directory, borgmatic_runtime_directory=None, skip_expand_paths=None
|
|
||||||
):
|
|
||||||
'''
|
'''
|
||||||
Given a sequence of Borg patterns, a configuration dict, a configured working directory, the
|
Given a sequence of Borg patterns, a configuration dict, a configured working directory, and a
|
||||||
borgmatic runtime directory, and a sequence of paths to skip path expansion for, expand and
|
sequence of paths to skip path expansion for, expand and deduplicate any "root" patterns,
|
||||||
deduplicate any "root" patterns, returning the resulting root and non-root patterns as a list.
|
returning the resulting root and non-root patterns as a list.
|
||||||
|
|
||||||
If the borgmatic runtime directory is None, then don't deduplicate patterns. Deduplication is
|
|
||||||
really only necessary for the "create" action when the runtime directory might contain named
|
|
||||||
pipes for database dumps.
|
|
||||||
'''
|
'''
|
||||||
skip_paths = set(skip_expand_paths or ())
|
skip_paths = set(skip_expand_paths or ())
|
||||||
|
|
||||||
return list(
|
return list(
|
||||||
deduplicate_runtime_directory_patterns(
|
deduplicate_patterns(
|
||||||
device_map_patterns(
|
device_map_patterns(
|
||||||
expand_patterns(
|
expand_patterns(
|
||||||
patterns,
|
patterns,
|
||||||
@@ -325,6 +294,5 @@ def process_patterns(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import borgmatic.borg.prune
|
import borgmatic.borg.prune
|
||||||
|
import borgmatic.config.validate
|
||||||
|
import borgmatic.hooks.command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -19,6 +21,12 @@ def run_prune(
|
|||||||
'''
|
'''
|
||||||
Run the "prune" action for the given repository.
|
Run the "prune" action for the given repository.
|
||||||
'''
|
'''
|
||||||
|
if prune_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||||
|
repository,
|
||||||
|
prune_arguments.repository,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
logger.info(f'Pruning archives{dry_run_label}')
|
logger.info(f'Pruning archives{dry_run_label}')
|
||||||
borgmatic.borg.prune.prune_archives(
|
borgmatic.borg.prune.prune_archives(
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import borgmatic.actions.pattern
|
import borgmatic.borg.info
|
||||||
import borgmatic.borg.pattern
|
|
||||||
import borgmatic.borg.recreate
|
import borgmatic.borg.recreate
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
from borgmatic.actions.pattern import collect_patterns, process_patterns
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,77 +19,70 @@ def run_recreate(
|
|||||||
local_borg_version,
|
local_borg_version,
|
||||||
recreate_arguments,
|
recreate_arguments,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
dry_run_label,
|
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Run the "recreate" action for the given repository.
|
Run the "recreate" action for the given repository.
|
||||||
'''
|
'''
|
||||||
if recreate_arguments.archive:
|
if recreate_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
logger.answer(f'Recreating archive {recreate_arguments.archive}{dry_run_label}')
|
repository,
|
||||||
else:
|
recreate_arguments.repository,
|
||||||
logger.answer(f'Recreating repository{dry_run_label}')
|
):
|
||||||
|
if recreate_arguments.archive:
|
||||||
|
logger.answer(f'Recreating archive {recreate_arguments.archive}')
|
||||||
|
else:
|
||||||
|
logger.answer('Recreating repository')
|
||||||
|
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
# Collect and process patterns.
|
||||||
|
processed_patterns = process_patterns(
|
||||||
# Collect and process patterns.
|
collect_patterns(config),
|
||||||
processed_patterns = borgmatic.actions.pattern.process_patterns(
|
config,
|
||||||
(
|
borgmatic.config.paths.get_working_directory(config),
|
||||||
*borgmatic.actions.pattern.collect_patterns(config, working_directory),
|
|
||||||
# Also add borgmatic-specific paths, so they don't get excluded from the recreated
|
|
||||||
# archive. Note that this doesn't currently work for archives created with Borg 1.2 or
|
|
||||||
# below.
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
|
||||||
'/borgmatic', source=borgmatic.borg.pattern.Pattern_source.INTERNAL
|
|
||||||
),
|
|
||||||
),
|
|
||||||
config,
|
|
||||||
working_directory,
|
|
||||||
)
|
|
||||||
|
|
||||||
archive = borgmatic.borg.repo_list.resolve_archive_name(
|
|
||||||
repository['path'],
|
|
||||||
recreate_arguments.archive,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
if archive and archive.endswith('.recreate'):
|
|
||||||
if recreate_arguments.archive == 'latest':
|
|
||||||
raise ValueError(
|
|
||||||
f'The latest archive "{archive}" is leftover from a prior recreate. Delete it first or select a different archive.',
|
|
||||||
)
|
|
||||||
|
|
||||||
raise ValueError(
|
|
||||||
f'The archive "{recreate_arguments.archive}" is leftover from a prior recreate. Select a different archive.',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
archive = borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
borgmatic.borg.recreate.recreate_archive(
|
|
||||||
repository['path'],
|
repository['path'],
|
||||||
archive,
|
recreate_arguments.archive,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
recreate_arguments,
|
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path=local_path,
|
local_path,
|
||||||
remote_path=remote_path,
|
remote_path,
|
||||||
patterns=processed_patterns,
|
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as error:
|
|
||||||
if error.returncode == BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS:
|
if archive and archive.endswith('.recreate'):
|
||||||
if recreate_arguments.target:
|
if recreate_arguments.archive == 'latest':
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'The archive "{recreate_arguments.target}" already exists. Delete it first or set a different target archive name.',
|
f'The latest archive "{archive}" is leftover from a prior recreate. Delete it first or select a different archive.',
|
||||||
)
|
)
|
||||||
|
|
||||||
if archive:
|
raise ValueError(
|
||||||
raise ValueError(
|
f'The archive "{recreate_arguments.archive}" is leftover from a prior recreate. Select a different archive.',
|
||||||
f'The archive "{archive}.recreate" is leftover from a prior recreate. Delete it first or select a different archive.',
|
)
|
||||||
)
|
|
||||||
|
|
||||||
raise
|
try:
|
||||||
|
borgmatic.borg.recreate.recreate_archive(
|
||||||
|
repository['path'],
|
||||||
|
archive,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
recreate_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
patterns=processed_patterns,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
if error.returncode == BORG_EXIT_CODE_ARCHIVE_ALREADY_EXISTS:
|
||||||
|
if recreate_arguments.target:
|
||||||
|
raise ValueError(
|
||||||
|
f'The archive "{recreate_arguments.target}" already exists. Delete it first or set a different target archive name.',
|
||||||
|
)
|
||||||
|
|
||||||
|
if archive:
|
||||||
|
raise ValueError(
|
||||||
|
f'The archive "{archive}.recreate" is leftover from a prior recreate. Delete it first or select a different archive.',
|
||||||
|
)
|
||||||
|
|
||||||
|
raise
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import borgmatic.borg.repo_create
|
import borgmatic.borg.repo_create
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,6 +18,12 @@ def run_repo_create(
|
|||||||
'''
|
'''
|
||||||
Run the "repo-create" action for the given repository.
|
Run the "repo-create" action for the given repository.
|
||||||
'''
|
'''
|
||||||
|
if repo_create_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||||
|
repository,
|
||||||
|
repo_create_arguments.repository,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
logger.info('Creating repository')
|
logger.info('Creating repository')
|
||||||
|
|
||||||
encryption_mode = repo_create_arguments.encryption_mode or repository.get('encryption')
|
encryption_mode = repo_create_arguments.encryption_mode or repository.get('encryption')
|
||||||
|
|||||||
@@ -17,16 +17,20 @@ def run_repo_delete(
|
|||||||
'''
|
'''
|
||||||
Run the "repo-delete" action for the given repository.
|
Run the "repo-delete" action for the given repository.
|
||||||
'''
|
'''
|
||||||
logger.answer(
|
if repo_delete_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
'Deleting repository' + (' cache' if repo_delete_arguments.cache_only else ''),
|
|
||||||
)
|
|
||||||
|
|
||||||
borgmatic.borg.repo_delete.delete_repository(
|
|
||||||
repository,
|
repository,
|
||||||
config,
|
repo_delete_arguments.repository,
|
||||||
local_borg_version,
|
):
|
||||||
repo_delete_arguments,
|
logger.answer(
|
||||||
global_arguments,
|
'Deleting repository' + (' cache' if repo_delete_arguments.cache_only else ''),
|
||||||
local_path,
|
)
|
||||||
remote_path,
|
|
||||||
)
|
borgmatic.borg.repo_delete.delete_repository(
|
||||||
|
repository,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
repo_delete_arguments,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.actions.json
|
import borgmatic.actions.json
|
||||||
import borgmatic.borg.repo_info
|
import borgmatic.borg.repo_info
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,17 +21,21 @@ def run_repo_info(
|
|||||||
|
|
||||||
If repo_info_arguments.json is True, yield the JSON output from the info for the repository.
|
If repo_info_arguments.json is True, yield the JSON output from the info for the repository.
|
||||||
'''
|
'''
|
||||||
if not repo_info_arguments.json:
|
if repo_info_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
logger.answer('Displaying repository summary information')
|
repository,
|
||||||
|
repo_info_arguments.repository,
|
||||||
|
):
|
||||||
|
if not repo_info_arguments.json:
|
||||||
|
logger.answer('Displaying repository summary information')
|
||||||
|
|
||||||
json_output = borgmatic.borg.repo_info.display_repository_info(
|
json_output = borgmatic.borg.repo_info.display_repository_info(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
repo_info_arguments=repo_info_arguments,
|
repo_info_arguments=repo_info_arguments,
|
||||||
global_arguments=global_arguments,
|
global_arguments=global_arguments,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
)
|
)
|
||||||
if json_output:
|
if json_output:
|
||||||
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import borgmatic.actions.json
|
import borgmatic.actions.json
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
|
import borgmatic.config.validate
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,17 +21,21 @@ def run_repo_list(
|
|||||||
|
|
||||||
If repo_list_arguments.json is True, yield the JSON output from listing the repository.
|
If repo_list_arguments.json is True, yield the JSON output from listing the repository.
|
||||||
'''
|
'''
|
||||||
if not repo_list_arguments.json:
|
if repo_list_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||||
logger.answer('Listing repository')
|
repository,
|
||||||
|
repo_list_arguments.repository,
|
||||||
|
):
|
||||||
|
if not repo_list_arguments.json:
|
||||||
|
logger.answer('Listing repository')
|
||||||
|
|
||||||
json_output = borgmatic.borg.repo_list.list_repository(
|
json_output = borgmatic.borg.repo_list.list_repository(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
repo_list_arguments=repo_list_arguments,
|
repo_list_arguments=repo_list_arguments,
|
||||||
global_arguments=global_arguments,
|
global_arguments=global_arguments,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
)
|
)
|
||||||
if json_output:
|
if json_output:
|
||||||
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
yield borgmatic.actions.json.parse_json(json_output, repository.get('label'))
|
||||||
|
|||||||
+142
-145
@@ -1,17 +1,16 @@
|
|||||||
import collections
|
import collections
|
||||||
import locale
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import borgmatic.actions.dump
|
|
||||||
import borgmatic.actions.pattern
|
|
||||||
import borgmatic.borg.extract
|
import borgmatic.borg.extract
|
||||||
import borgmatic.borg.list
|
import borgmatic.borg.list
|
||||||
|
import borgmatic.borg.mount
|
||||||
import borgmatic.borg.repo_list
|
import borgmatic.borg.repo_list
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
|
import borgmatic.config.validate
|
||||||
import borgmatic.hooks.data_source.dump
|
import borgmatic.hooks.data_source.dump
|
||||||
import borgmatic.hooks.dispatch
|
import borgmatic.hooks.dispatch
|
||||||
|
|
||||||
@@ -131,14 +130,14 @@ def get_configured_data_source(config, restore_dump):
|
|||||||
|
|
||||||
|
|
||||||
def strip_path_prefix_from_extracted_dump_destination(
|
def strip_path_prefix_from_extracted_dump_destination(
|
||||||
extract_path,
|
destination_path,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Directory-format dump files get extracted into a temporary directory containing a path prefix
|
Directory-format dump files get extracted into a temporary directory containing a path prefix
|
||||||
that depends how the files were stored in the archive. So, given the path where the dump was
|
that depends how the files were stored in the archive. So, given the destination path where the
|
||||||
extracted and the borgmatic runtime directory, move the dump files such that the restore doesn't
|
dump was extracted and the borgmatic runtime directory, move the dump files such that the
|
||||||
have to deal with that varying path prefix.
|
restore doesn't have to deal with that varying path prefix.
|
||||||
|
|
||||||
For instance, if the dump was extracted to:
|
For instance, if the dump was extracted to:
|
||||||
|
|
||||||
@@ -152,16 +151,16 @@ def strip_path_prefix_from_extracted_dump_destination(
|
|||||||
|
|
||||||
/run/user/0/borgmatic/postgresql_databases/test/...
|
/run/user/0/borgmatic/postgresql_databases/test/...
|
||||||
'''
|
'''
|
||||||
for subdirectory_path, _, _ in os.walk(extract_path):
|
for subdirectory_path, _, _ in os.walk(destination_path):
|
||||||
databases_directory = os.path.basename(subdirectory_path)
|
databases_directory = os.path.basename(subdirectory_path)
|
||||||
|
|
||||||
if not databases_directory.endswith('_databases'):
|
if not databases_directory.endswith('_databases'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
destination_path = os.path.join(borgmatic_runtime_directory, databases_directory)
|
shutil.move(
|
||||||
shutil.rmtree(destination_path, ignore_errors=True)
|
subdirectory_path,
|
||||||
shutil.move(subdirectory_path, destination_path)
|
os.path.join(borgmatic_runtime_directory, databases_directory),
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
@@ -202,10 +201,6 @@ def restore_single_dump(
|
|||||||
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
data_source['name'],
|
data_source['name'],
|
||||||
data_source.get('hostname'),
|
|
||||||
data_source.get('port'),
|
|
||||||
data_source.get('container'),
|
|
||||||
data_source.get('label'),
|
|
||||||
)[hook_name.split('_databases', 1)[0]]
|
)[hook_name.split('_databases', 1)[0]]
|
||||||
|
|
||||||
destination_path = (
|
destination_path = (
|
||||||
@@ -273,12 +268,12 @@ def collect_dumps_from_archive(
|
|||||||
Given a local or remote repository path, a resolved archive name, a configuration dict, the
|
Given a local or remote repository path, a resolved archive name, a configuration dict, the
|
||||||
local Borg version, global arguments an argparse.Namespace, local and remote Borg paths, and the
|
local Borg version, global arguments an argparse.Namespace, local and remote Borg paths, and the
|
||||||
borgmatic runtime directory, query the archive for the names of data sources dumps it contains
|
borgmatic runtime directory, query the archive for the names of data sources dumps it contains
|
||||||
and return them as a tuple of Dump instances.
|
and return them as a set of Dump instances.
|
||||||
'''
|
'''
|
||||||
dumps_from_archive = {} # Use a dict as an ordered set.
|
dumps_from_archive = set()
|
||||||
|
|
||||||
# There is (at most) one dump metadata file per data source hook. Load each.
|
# There is (at most) one dump metadata file per data source hook. Load each.
|
||||||
for dumps_metadata_entry in borgmatic.borg.list.capture_archive_listing(
|
for dumps_metadata_path in borgmatic.borg.list.capture_archive_listing(
|
||||||
repository,
|
repository,
|
||||||
archive,
|
archive,
|
||||||
config,
|
config,
|
||||||
@@ -301,33 +296,36 @@ def collect_dumps_from_archive(
|
|||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
):
|
):
|
||||||
if not dumps_metadata_entry.get('path'):
|
if not dumps_metadata_path:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for dump in borgmatic.hooks.data_source.dump.parse_data_source_dumps_metadata(
|
dumps_from_archive.update(
|
||||||
borgmatic.borg.extract.extract_archive(
|
set(
|
||||||
global_arguments.dry_run,
|
borgmatic.hooks.data_source.dump.parse_data_source_dumps_metadata(
|
||||||
repository,
|
borgmatic.borg.extract.extract_archive(
|
||||||
archive,
|
global_arguments.dry_run,
|
||||||
[dumps_metadata_entry['path']],
|
repository,
|
||||||
config,
|
archive,
|
||||||
local_borg_version,
|
[dumps_metadata_path],
|
||||||
global_arguments,
|
config,
|
||||||
local_path=local_path,
|
local_borg_version,
|
||||||
remote_path=remote_path,
|
global_arguments,
|
||||||
extract_to_stdout=True,
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
extract_to_stdout=True,
|
||||||
|
)
|
||||||
|
.stdout.read()
|
||||||
|
.decode(),
|
||||||
|
dumps_metadata_path,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.stdout.read()
|
)
|
||||||
.decode(locale.getpreferredencoding()),
|
|
||||||
dumps_metadata_entry['path'],
|
|
||||||
):
|
|
||||||
dumps_from_archive[dump] = None
|
|
||||||
|
|
||||||
# If we've successfully loaded any dumps metadata, we're done.
|
# If we've successfully loaded any dumps metadata, we're done.
|
||||||
if dumps_from_archive:
|
if dumps_from_archive:
|
||||||
logger.debug('Collecting database dumps from archive data source dumps metadata files')
|
logger.debug('Collecting database dumps from archive data source dumps metadata files')
|
||||||
|
|
||||||
return tuple(dumps_from_archive.keys())
|
return dumps_from_archive
|
||||||
|
|
||||||
# No dumps metadata files were found, so for backwards compatibility, fall back to parsing the
|
# No dumps metadata files were found, so for backwards compatibility, fall back to parsing the
|
||||||
# paths of dumps found in the archive to get their respective dump metadata.
|
# paths of dumps found in the archive to get their respective dump metadata.
|
||||||
@@ -339,7 +337,7 @@ def collect_dumps_from_archive(
|
|||||||
# Probe for the data source dumps in multiple locations, as the default location has moved to
|
# Probe for the data source dumps in multiple locations, as the default location has moved to
|
||||||
# the borgmatic runtime directory (which gets stored as just "/borgmatic" with Borg 1.4+). But
|
# the borgmatic runtime directory (which gets stored as just "/borgmatic" with Borg 1.4+). But
|
||||||
# we still want to support reading dumps from previously created archives as well.
|
# we still want to support reading dumps from previously created archives as well.
|
||||||
dump_entries = borgmatic.borg.list.capture_archive_listing(
|
dump_paths = borgmatic.borg.list.capture_archive_listing(
|
||||||
repository,
|
repository,
|
||||||
archive,
|
archive,
|
||||||
config,
|
config,
|
||||||
@@ -361,9 +359,7 @@ def collect_dumps_from_archive(
|
|||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
for dump_entry in dump_entries:
|
for dump_path in dump_paths:
|
||||||
dump_path = dump_entry.get('path')
|
|
||||||
|
|
||||||
if not dump_path:
|
if not dump_path:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -393,11 +389,11 @@ def collect_dumps_from_archive(
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
port = None
|
port = None
|
||||||
|
|
||||||
dumps_from_archive[
|
dumps_from_archive.add(
|
||||||
Dump(
|
Dump(
|
||||||
hook_name, data_source_name, None if hostname == 'localhost' else hostname, port
|
hook_name, data_source_name, None if hostname == 'localhost' else hostname, port
|
||||||
)
|
)
|
||||||
] = None
|
)
|
||||||
|
|
||||||
# We've successfully parsed the dump path, so need to probe any further.
|
# We've successfully parsed the dump path, so need to probe any further.
|
||||||
break
|
break
|
||||||
@@ -406,21 +402,20 @@ def collect_dumps_from_archive(
|
|||||||
f'Ignoring invalid data source dump path "{dump_path}" in archive {archive}',
|
f'Ignoring invalid data source dump path "{dump_path}" in archive {archive}',
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(dumps_from_archive.keys())
|
return dumps_from_archive
|
||||||
|
|
||||||
|
|
||||||
def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
||||||
'''
|
'''
|
||||||
Given restore arguments as an argparse.Namespace instance indicating which dumps to restore and
|
Given restore arguments as an argparse.Namespace instance indicating which dumps to restore and
|
||||||
a tuple of Dump instances representing the dumps found in an archive, return a tuple of specific
|
a set of Dump instances representing the dumps found in an archive, return a set of specific
|
||||||
Dump instances from the archive to restore. As part of this, replace any Dump having a data
|
Dump instances from the archive to restore. As part of this, replace any Dump having a data
|
||||||
source name of "all" with multiple named Dump instances as appropriate.
|
source name of "all" with multiple named Dump instances as appropriate.
|
||||||
|
|
||||||
Raise ValueError if any of the requested data source names cannot be found in the archive or if
|
Raise ValueError if any of the requested data source names cannot be found in the archive or if
|
||||||
there are multiple archive dump matches for a given requested dump.
|
there are multiple archive dump matches for a given requested dump.
|
||||||
'''
|
'''
|
||||||
requested_dumps = tuple(
|
requested_dumps = (
|
||||||
# Use a dict comprehension as an ordered set.
|
|
||||||
{
|
{
|
||||||
Dump(
|
Dump(
|
||||||
hook_name=(
|
hook_name=(
|
||||||
@@ -437,16 +432,16 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
|||||||
port=restore_arguments.original_port,
|
port=restore_arguments.original_port,
|
||||||
label=restore_arguments.original_label or UNSPECIFIED,
|
label=restore_arguments.original_label or UNSPECIFIED,
|
||||||
container=restore_arguments.original_container or UNSPECIFIED,
|
container=restore_arguments.original_container or UNSPECIFIED,
|
||||||
): None
|
)
|
||||||
for name in restore_arguments.data_sources or (UNSPECIFIED,)
|
for name in restore_arguments.data_sources or (UNSPECIFIED,)
|
||||||
}.keys()
|
}
|
||||||
if restore_arguments.hook
|
if restore_arguments.hook
|
||||||
or restore_arguments.data_sources
|
or restore_arguments.data_sources
|
||||||
or restore_arguments.original_hostname
|
or restore_arguments.original_hostname
|
||||||
or restore_arguments.original_port
|
or restore_arguments.original_port
|
||||||
or restore_arguments.original_label
|
or restore_arguments.original_label
|
||||||
or restore_arguments.original_container
|
or restore_arguments.original_container
|
||||||
else (
|
else {
|
||||||
Dump(
|
Dump(
|
||||||
hook_name=UNSPECIFIED,
|
hook_name=UNSPECIFIED,
|
||||||
data_source_name='all',
|
data_source_name='all',
|
||||||
@@ -455,15 +450,14 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
|||||||
label=UNSPECIFIED,
|
label=UNSPECIFIED,
|
||||||
container=UNSPECIFIED,
|
container=UNSPECIFIED,
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
)
|
)
|
||||||
missing_dumps = set()
|
missing_dumps = set()
|
||||||
dumps_to_restore = {} # Use a dict as an ordered set.
|
dumps_to_restore = set()
|
||||||
|
|
||||||
# If there's a requested "all" dump, add every dump from the archive to the dumps to restore.
|
# If there's a requested "all" dump, add every dump from the archive to the dumps to restore.
|
||||||
if any(dump for dump in requested_dumps if dump.data_source_name == 'all'):
|
if any(dump for dump in requested_dumps if dump.data_source_name == 'all'):
|
||||||
for dump in dumps_from_archive:
|
dumps_to_restore.update(dumps_from_archive)
|
||||||
dumps_to_restore[dump] = None
|
|
||||||
|
|
||||||
# If any archive dump matches a requested dump, add the archive dump to the dumps to restore.
|
# If any archive dump matches a requested dump, add the archive dump to the dumps to restore.
|
||||||
for requested_dump in requested_dumps:
|
for requested_dump in requested_dumps:
|
||||||
@@ -479,7 +473,7 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
|||||||
if len(matching_dumps) == 0:
|
if len(matching_dumps) == 0:
|
||||||
missing_dumps.add(requested_dump)
|
missing_dumps.add(requested_dump)
|
||||||
elif len(matching_dumps) == 1:
|
elif len(matching_dumps) == 1:
|
||||||
dumps_to_restore[matching_dumps[0]] = None
|
dumps_to_restore.add(matching_dumps[0])
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Cannot restore data source {render_dump_metadata(requested_dump)} because there are multiple matching dumps in the archive. Try adding flags to disambiguate.',
|
f'Cannot restore data source {render_dump_metadata(requested_dump)} because there are multiple matching dumps in the archive. Try adding flags to disambiguate.',
|
||||||
@@ -494,20 +488,20 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
|
|||||||
f"Cannot restore data source dump{'s' if len(missing_dumps) > 1 else ''} {rendered_dumps} missing from archive",
|
f"Cannot restore data source dump{'s' if len(missing_dumps) > 1 else ''} {rendered_dumps} missing from archive",
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(dumps_to_restore.keys())
|
return dumps_to_restore
|
||||||
|
|
||||||
|
|
||||||
def ensure_requested_dumps_restored(dumps_to_restore, dumps_actually_restored):
|
def ensure_requested_dumps_restored(dumps_to_restore, dumps_actually_restored):
|
||||||
'''
|
'''
|
||||||
Given a tuple of requested dumps to restore and a set of dumps actually restored, raise
|
Given a set of requested dumps to restore and a set of dumps actually restored, raise ValueError
|
||||||
ValueError if any requested dumps to restore weren't restored, indicating that they were missing
|
if any requested dumps to restore weren't restored, indicating that they were missing from the
|
||||||
from the configuration.
|
configuration.
|
||||||
'''
|
'''
|
||||||
if not dumps_to_restore:
|
if not dumps_actually_restored:
|
||||||
raise ValueError('No data source dumps were found to restore')
|
raise ValueError('No data source dumps were found to restore')
|
||||||
|
|
||||||
missing_dumps = sorted(
|
missing_dumps = sorted(
|
||||||
set(dumps_to_restore) - dumps_actually_restored,
|
dumps_to_restore - dumps_actually_restored,
|
||||||
key=lambda dump: dump.data_source_name,
|
key=lambda dump: dump.data_source_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -529,106 +523,109 @@ def run_restore(
|
|||||||
remote_path,
|
remote_path,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Run the "restore" action for the given repository.
|
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 or there's no
|
Raise ValueError if a configured data source could not be found to restore or there's no
|
||||||
matching dump in the archive.
|
matching dump in the archive.
|
||||||
'''
|
'''
|
||||||
logger.info(f'Restoring data sources from archive {restore_arguments.archive}')
|
if restore_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
repository,
|
||||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
restore_arguments.repository,
|
||||||
repository['path'],
|
):
|
||||||
config,
|
return
|
||||||
local_borg_version,
|
|
||||||
global_arguments,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
with borgmatic.config.paths.Runtime_directory(
|
logger.info(f'Restoring data sources from archive {restore_arguments.archive}')
|
||||||
config, repository_id
|
|
||||||
) as borgmatic_runtime_directory:
|
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||||
patterns = borgmatic.actions.pattern.process_patterns(
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
borgmatic.actions.pattern.collect_patterns(config, working_directory),
|
'remove_data_source_dumps',
|
||||||
config,
|
config,
|
||||||
working_directory,
|
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||||
|
borgmatic_runtime_directory,
|
||||||
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
with borgmatic.actions.dump.Dump_cleanup(
|
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
||||||
config, borgmatic_runtime_directory, patterns, global_arguments.dry_run
|
repository['path'],
|
||||||
):
|
restore_arguments.archive,
|
||||||
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
config,
|
||||||
repository['path'],
|
local_borg_version,
|
||||||
restore_arguments.archive,
|
global_arguments,
|
||||||
config,
|
local_path,
|
||||||
local_borg_version,
|
remote_path,
|
||||||
global_arguments,
|
)
|
||||||
local_path,
|
dumps_from_archive = collect_dumps_from_archive(
|
||||||
remote_path,
|
repository['path'],
|
||||||
)
|
archive_name,
|
||||||
dumps_from_archive = collect_dumps_from_archive(
|
config,
|
||||||
repository['path'],
|
local_borg_version,
|
||||||
archive_name,
|
global_arguments,
|
||||||
config,
|
local_path,
|
||||||
local_borg_version,
|
remote_path,
|
||||||
global_arguments,
|
borgmatic_runtime_directory,
|
||||||
local_path,
|
)
|
||||||
remote_path,
|
dumps_to_restore = get_dumps_to_restore(restore_arguments, dumps_from_archive)
|
||||||
borgmatic_runtime_directory,
|
|
||||||
)
|
|
||||||
dumps_to_restore = get_dumps_to_restore(restore_arguments, dumps_from_archive)
|
|
||||||
|
|
||||||
dumps_actually_restored = set()
|
dumps_actually_restored = set()
|
||||||
connection_params = {
|
connection_params = {
|
||||||
'container': restore_arguments.container,
|
'container': restore_arguments.container,
|
||||||
'hostname': restore_arguments.hostname,
|
'hostname': restore_arguments.hostname,
|
||||||
'port': restore_arguments.port,
|
'port': restore_arguments.port,
|
||||||
'username': restore_arguments.username,
|
'username': restore_arguments.username,
|
||||||
'password': restore_arguments.password,
|
'password': restore_arguments.password,
|
||||||
'restore_path': restore_arguments.restore_path,
|
'restore_path': restore_arguments.restore_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Restore each dump.
|
# Restore each dump.
|
||||||
for restore_dump in dumps_to_restore:
|
for restore_dump in dumps_to_restore:
|
||||||
|
found_data_source = get_configured_data_source(
|
||||||
|
config,
|
||||||
|
restore_dump,
|
||||||
|
)
|
||||||
|
# For a dump that wasn't found via an exact match in the configuration, try to fallback
|
||||||
|
# to an "all" data source.
|
||||||
|
if not found_data_source:
|
||||||
found_data_source = get_configured_data_source(
|
found_data_source = get_configured_data_source(
|
||||||
config,
|
config,
|
||||||
restore_dump,
|
Dump(
|
||||||
|
restore_dump.hook_name,
|
||||||
|
'all',
|
||||||
|
restore_dump.hostname,
|
||||||
|
restore_dump.port,
|
||||||
|
restore_dump.label,
|
||||||
|
restore_dump.container,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
# For a dump that wasn't found via an exact match in the configuration, try to fallback
|
|
||||||
# to an "all" data source.
|
|
||||||
if not found_data_source:
|
if not found_data_source:
|
||||||
found_data_source = get_configured_data_source(
|
continue
|
||||||
config,
|
|
||||||
Dump(
|
|
||||||
restore_dump.hook_name,
|
|
||||||
'all',
|
|
||||||
restore_dump.hostname,
|
|
||||||
restore_dump.port,
|
|
||||||
restore_dump.label,
|
|
||||||
restore_dump.container,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not found_data_source:
|
found_data_source = dict(found_data_source)
|
||||||
continue
|
found_data_source['name'] = restore_dump.data_source_name
|
||||||
|
|
||||||
found_data_source = dict(found_data_source)
|
dumps_actually_restored.add(restore_dump)
|
||||||
found_data_source['name'] = restore_dump.data_source_name
|
|
||||||
|
|
||||||
dumps_actually_restored.add(restore_dump)
|
restore_single_dump(
|
||||||
|
repository,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
archive_name,
|
||||||
|
restore_dump.hook_name,
|
||||||
|
dict(found_data_source, schemas=restore_arguments.schemas),
|
||||||
|
connection_params,
|
||||||
|
borgmatic_runtime_directory,
|
||||||
|
)
|
||||||
|
|
||||||
restore_single_dump(
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
repository,
|
'remove_data_source_dumps',
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
borgmatic.hooks.dispatch.Hook_type.DATA_SOURCE,
|
||||||
global_arguments,
|
borgmatic_runtime_directory,
|
||||||
local_path,
|
global_arguments.dry_run,
|
||||||
remote_path,
|
)
|
||||||
archive_name,
|
|
||||||
restore_dump.hook_name,
|
|
||||||
dict(found_data_source, schemas=restore_arguments.schemas),
|
|
||||||
connection_params,
|
|
||||||
borgmatic_runtime_directory,
|
|
||||||
)
|
|
||||||
|
|
||||||
ensure_requested_dumps_restored(dumps_to_restore, dumps_actually_restored)
|
ensure_requested_dumps_restored(dumps_to_restore, dumps_actually_restored)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
from borgmatic.borg import environment, flags
|
from borgmatic.borg import environment, flags
|
||||||
@@ -23,17 +22,15 @@ def break_lock(
|
|||||||
'''
|
'''
|
||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('break_lock', '')
|
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'break-lock')
|
(local_path, 'break-lock')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ ('--log-json',)
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.execute
|
import borgmatic.execute
|
||||||
@@ -26,16 +25,15 @@ def change_passphrase(
|
|||||||
borgmatic.logger.add_custom_log_levels()
|
borgmatic.logger.add_custom_log_levels()
|
||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('key_change_passphrase', '')
|
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'key', 'change-passphrase')
|
(local_path, 'key', 'change-passphrase')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(
|
+ flags.make_repository_flags(
|
||||||
repository_path,
|
repository_path,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
@@ -65,5 +63,5 @@ def change_passphrase(
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.answer(
|
logger.answer(
|
||||||
"Don't forget to update your encryption_passphrase option (if needed)",
|
f"{repository_path}: Don't forget to update your encryption_passphrase option (if needed)",
|
||||||
)
|
)
|
||||||
|
|||||||
+35
-11
@@ -1,8 +1,10 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags, repo_info
|
||||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -80,6 +82,36 @@ def make_check_name_flags(checks, archive_filter_flags):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_id(
|
||||||
|
repository_path,
|
||||||
|
config,
|
||||||
|
local_borg_version,
|
||||||
|
global_arguments,
|
||||||
|
local_path,
|
||||||
|
remote_path,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
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.
|
||||||
|
|
||||||
|
Raise ValueError if the Borg repository ID cannot be determined.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
return json.loads(
|
||||||
|
repo_info.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}')
|
||||||
|
|
||||||
|
|
||||||
def check_archives(
|
def check_archives(
|
||||||
repository_path,
|
repository_path,
|
||||||
config,
|
config,
|
||||||
@@ -117,10 +149,8 @@ def check_archives(
|
|||||||
|
|
||||||
max_duration = check_arguments.max_duration or repository_check_config.get('max_duration')
|
max_duration = check_arguments.max_duration or repository_check_config.get('max_duration')
|
||||||
|
|
||||||
# If not configured, elevate Borg's exit code 1 (an ostensible warning) to error, because Borg
|
|
||||||
# returns exit code 1 for repository check errors!
|
|
||||||
borg_exit_codes = [*config.get('borg_exit_codes', []), *[{'code': 1, 'treat_as': 'error'}]]
|
|
||||||
umask = config.get('umask')
|
umask = config.get('umask')
|
||||||
|
borg_exit_codes = config.get('borg_exit_codes')
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
|
|
||||||
if 'data' in checks:
|
if 'data' in checks:
|
||||||
@@ -146,13 +176,7 @@ def check_archives(
|
|||||||
+ make_check_name_flags(checks_subset, archive_filter_flags)
|
+ make_check_name_flags(checks_subset, archive_filter_flags)
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
('--log-json',)
|
|
||||||
if (
|
|
||||||
config.get('log_json') or not (check_arguments.repair or config.get('progress'))
|
|
||||||
)
|
|
||||||
else ()
|
|
||||||
)
|
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ verbosity_flags
|
+ verbosity_flags
|
||||||
+ (('--progress',) if config.get('progress') else ())
|
+ (('--progress',) if config.get('progress') else ())
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def compact_segments(
|
|||||||
(local_path, 'compact')
|
(local_path, 'compact')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (('--log-json',) if (config.get('log_json') or not config.get('progress')) else ())
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--progress',) if config.get('progress') else ())
|
+ (('--progress',) if config.get('progress') else ())
|
||||||
+ (('--cleanup-commits',) if cleanup_commits else ())
|
+ (('--cleanup-commits',) if cleanup_commits else ())
|
||||||
|
|||||||
+71
-118
@@ -53,7 +53,6 @@ def validate_planned_backup_paths(
|
|||||||
local_path,
|
local_path,
|
||||||
working_directory,
|
working_directory,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
find_special_files=False,
|
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Given a dry-run flag, a Borg create command as a tuple, a configuration dict, a local Borg path,
|
Given a dry-run flag, a Borg create command as a tuple, a configuration dict, a local Borg path,
|
||||||
@@ -62,25 +61,18 @@ def validate_planned_backup_paths(
|
|||||||
given runtime directory exists, validate that it will be included in a backup and hasn't been
|
given runtime directory exists, validate that it will be included in a backup and hasn't been
|
||||||
excluded.
|
excluded.
|
||||||
|
|
||||||
If find special files is True, then return the subset of planned backup paths that are special
|
|
||||||
files. Otherwise, return an empty tuple.
|
|
||||||
|
|
||||||
Raise ValueError if the runtime directory has been excluded via "exclude_patterns" or similar,
|
Raise ValueError if the runtime directory has been excluded via "exclude_patterns" or similar,
|
||||||
because any features that rely on the runtime directory getting backed up will break. For
|
because any features that rely on the runtime directory getting backed up will break. For
|
||||||
instance, without the runtime directory, Borg can't consume any database dumps and borgmatic may
|
instance, without the runtime directory, Borg can't consume any database dumps and borgmatic may
|
||||||
hang waiting for them to be consumed.
|
hang waiting for them to be consumed.
|
||||||
'''
|
'''
|
||||||
# Omit "--exclude-nodump" from the Borg dry run command, because that flag causes Borg to open
|
# Omit "--exclude-nodump" from the Borg dry run command, because that flag causes Borg to open
|
||||||
# files including any named pipe we've created. And omit "--filter" because that can break the
|
# files including any named pipe we've created. And omit "--filter" because that can break the
|
||||||
# paths output parsing below such that path lines no longer start with the expected "- ".
|
# paths output parsing below such that path lines no longer start with the expected "- ".
|
||||||
path_lines = execute_command_and_capture_output(
|
paths_output = execute_command_and_capture_output(
|
||||||
(
|
(
|
||||||
*flags.omit_flag_and_value(
|
*flags.omit_flag_and_value(
|
||||||
flags.omit_flag(
|
flags.omit_flag(create_command, '--exclude-nodump'), '--filter'
|
||||||
flags.omit_flag(create_command, '--exclude-nodump'),
|
|
||||||
'--log-json',
|
|
||||||
),
|
|
||||||
'--filter',
|
|
||||||
),
|
),
|
||||||
'--dry-run',
|
'--dry-run',
|
||||||
'--list',
|
'--list',
|
||||||
@@ -94,68 +86,44 @@ def validate_planned_backup_paths(
|
|||||||
|
|
||||||
# These are all the individual files that Borg is planning to backup as determined by the Borg
|
# These are all the individual files that Borg is planning to backup as determined by the Borg
|
||||||
# create dry run above.
|
# create dry run above.
|
||||||
paths = (
|
paths = tuple(
|
||||||
path_line.split(' ', 1)[1]
|
path_line.split(' ', 1)[1]
|
||||||
for path_line in path_lines
|
for path_line in paths_output.split('\n')
|
||||||
if path_line and path_line.startswith(('- ', '+ '))
|
if path_line and path_line.startswith(('- ', '+ '))
|
||||||
)
|
)
|
||||||
|
|
||||||
include_pattern_paths = {
|
# These are the subset of output paths contained within the borgmatic runtime directory.
|
||||||
pattern.path
|
paths_inside_runtime_directory = {
|
||||||
for pattern in patterns
|
path for path in paths if any_parent_directories(path, (borgmatic_runtime_directory,))
|
||||||
if pattern.type == borgmatic.borg.pattern.Pattern_type.INCLUDE
|
|
||||||
}
|
}
|
||||||
runtime_directory_root_patterns = tuple(
|
|
||||||
|
# If the runtime directory isn't present in the source patterns, then we shouldn't expect it to
|
||||||
|
# be in the paths output from the Borg dry run.
|
||||||
|
runtime_directory_present_in_patterns = any(
|
||||||
pattern
|
pattern
|
||||||
for pattern in patterns
|
for pattern in patterns
|
||||||
if any_parent_directories(pattern.path, (borgmatic_runtime_directory,))
|
if any_parent_directories(pattern.path, (borgmatic_runtime_directory,))
|
||||||
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
|
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
|
||||||
# Skip root patterns that have corresponding include patterns, because those will "punch
|
|
||||||
# through" any subsequent excludes.
|
|
||||||
if pattern.path not in include_pattern_paths
|
|
||||||
)
|
)
|
||||||
|
|
||||||
special_paths = []
|
# If no paths to backup are inside the runtime directory, it must've been excluded.
|
||||||
validate_runtime_directory = bool(
|
if (
|
||||||
not dry_run
|
not paths_inside_runtime_directory
|
||||||
|
and runtime_directory_present_in_patterns
|
||||||
|
and not dry_run
|
||||||
and os.path.exists(borgmatic_runtime_directory)
|
and os.path.exists(borgmatic_runtime_directory)
|
||||||
and runtime_directory_root_patterns
|
):
|
||||||
)
|
raise ValueError(
|
||||||
runtime_directory_in_path = False
|
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes or patterns with excludes. Please ensure the runtime directory is not excluded.',
|
||||||
|
|
||||||
# Do everything in this one loop because we only want to consume the paths generator once.
|
|
||||||
for path in paths:
|
|
||||||
# If all root patterns in the runtime directory are missing from the paths Borg is planning
|
|
||||||
# to backup, then they must've gotten excluded, e.g. by user-configured excludes. Warn
|
|
||||||
# accordingly (below).
|
|
||||||
if validate_runtime_directory and any(
|
|
||||||
any_parent_directories(path, (pattern.path,))
|
|
||||||
for pattern in runtime_directory_root_patterns
|
|
||||||
):
|
|
||||||
runtime_directory_in_path = True
|
|
||||||
|
|
||||||
# Return the subset of output paths that are special files but *not* contained within the
|
|
||||||
# borgmatic runtime directory. The intent is to skip runtime paths that borgmatic uses for its
|
|
||||||
# own bookkeeping, instead focusing on user-configured paths.
|
|
||||||
if (
|
|
||||||
find_special_files
|
|
||||||
and not any_parent_directories(path, (borgmatic_runtime_directory,))
|
|
||||||
and special_file(path, working_directory)
|
|
||||||
):
|
|
||||||
special_paths.append(path)
|
|
||||||
|
|
||||||
if validate_runtime_directory and not runtime_directory_in_path:
|
|
||||||
logger.warning(
|
|
||||||
f'The runtime directory {os.path.normpath(borgmatic_runtime_directory)} overlaps with the configured excludes (or the snapshotted source directories are empty). Please ensure the runtime directory is not excluded.'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(special_paths)
|
return tuple(path for path in paths if path not in paths_inside_runtime_directory)
|
||||||
|
|
||||||
|
|
||||||
MAX_SPECIAL_FILE_PATHS_LENGTH = 1000
|
MAX_SPECIAL_FILE_PATHS_LENGTH = 1000
|
||||||
|
|
||||||
|
|
||||||
def make_base_create_command( # noqa: PLR0912
|
def make_base_create_command(
|
||||||
dry_run,
|
dry_run,
|
||||||
repository_path,
|
repository_path,
|
||||||
config,
|
config,
|
||||||
@@ -179,10 +147,8 @@ def make_base_create_command( # noqa: PLR0912
|
|||||||
return a tuple of (base Borg create command flags, Borg create command positional arguments,
|
return a tuple of (base Borg create command flags, Borg create command positional arguments,
|
||||||
open pattern file handle).
|
open pattern file handle).
|
||||||
'''
|
'''
|
||||||
if config.get('source_directories_must_exist', True):
|
if config.get('source_directories_must_exist', False):
|
||||||
borgmatic.borg.pattern.check_all_root_patterns_exist(
|
borgmatic.borg.pattern.check_all_root_patterns_exist(patterns)
|
||||||
patterns, borgmatic.config.paths.get_working_directory(config)
|
|
||||||
)
|
|
||||||
|
|
||||||
patterns_file = borgmatic.borg.pattern.write_patterns_file(
|
patterns_file = borgmatic.borg.pattern.write_patterns_file(
|
||||||
patterns,
|
patterns,
|
||||||
@@ -197,7 +163,6 @@ def make_base_create_command( # noqa: PLR0912
|
|||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run)
|
list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run)
|
||||||
files_changed = config.get('files_changed')
|
|
||||||
files_cache = config.get('files_cache')
|
files_cache = config.get('files_cache')
|
||||||
archive_name_format = (
|
archive_name_format = (
|
||||||
config.get('archive_name_format', flags.get_default_archive_name_format(local_borg_version))
|
config.get('archive_name_format', flags.get_default_archive_name_format(local_borg_version))
|
||||||
@@ -248,12 +213,11 @@ def make_base_create_command( # noqa: PLR0912
|
|||||||
+ (('--nobirthtime',) if config.get('birthtime') is False else ())
|
+ (('--nobirthtime',) if config.get('birthtime') is False else ())
|
||||||
+ (('--read-special',) if config.get('read_special') or stream_processes else ())
|
+ (('--read-special',) if config.get('read_special') or stream_processes else ())
|
||||||
+ noflags_flags
|
+ noflags_flags
|
||||||
+ (('--files-changed', files_changed) if files_changed else ())
|
|
||||||
+ (('--files-cache', files_cache) if files_cache else ())
|
+ (('--files-cache', files_cache) if files_cache else ())
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--log-json',) if (config.get('log_json') or not config.get('progress')) else ())
|
|
||||||
+ (
|
+ (
|
||||||
('--list', '--filter', list_filter_flags)
|
('--list', '--filter', list_filter_flags)
|
||||||
if config.get('list_details') and not json and not config.get('progress')
|
if config.get('list_details') and not json and not config.get('progress')
|
||||||
@@ -270,16 +234,8 @@ def make_base_create_command( # noqa: PLR0912
|
|||||||
)
|
)
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
|
|
||||||
if config.get('unsafe_skip_path_validation_before_create'):
|
|
||||||
logger.debug(
|
|
||||||
'Skipping pre-backup path validation due to "unsafe_skip_path_validation_before_create" option.'
|
|
||||||
)
|
|
||||||
|
|
||||||
return (create_flags, create_positional_arguments, patterns_file)
|
|
||||||
|
|
||||||
logger.debug('Checking file paths Borg plans to include')
|
logger.debug('Checking file paths Borg plans to include')
|
||||||
|
planned_backup_paths = validate_planned_backup_paths(
|
||||||
special_file_paths = validate_planned_backup_paths(
|
|
||||||
dry_run,
|
dry_run,
|
||||||
create_flags + create_positional_arguments,
|
create_flags + create_positional_arguments,
|
||||||
config,
|
config,
|
||||||
@@ -287,41 +243,44 @@ def make_base_create_command( # noqa: PLR0912
|
|||||||
local_path,
|
local_path,
|
||||||
working_directory,
|
working_directory,
|
||||||
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
borgmatic_runtime_directory=borgmatic_runtime_directory,
|
||||||
find_special_files=bool(stream_processes),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if stream_processes and config.get('read_special') is False:
|
# 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(
|
logger.warning(
|
||||||
'Ignoring configured "read_special" value of false, as true is needed for database hooks.',
|
'Ignoring configured "read_special" value of false, as true is needed for database hooks.',
|
||||||
)
|
)
|
||||||
|
|
||||||
# If database hooks are enabled (as indicated by streaming processes), exclude files that might
|
special_file_paths = tuple(
|
||||||
# cause Borg to hang. But skip this if the user has explicitly set the "read_special" to True.
|
path for path in planned_backup_paths if special_file(path, working_directory)
|
||||||
if special_file_paths:
|
|
||||||
truncated_special_file_paths = textwrap.shorten(
|
|
||||||
', '.join(special_file_paths),
|
|
||||||
width=MAX_SPECIAL_FILE_PATHS_LENGTH,
|
|
||||||
placeholder=' ...',
|
|
||||||
)
|
|
||||||
logger.warning(
|
|
||||||
f'Excluding special files to prevent Borg from hanging: {truncated_special_file_paths}',
|
|
||||||
)
|
|
||||||
patterns_file = borgmatic.borg.pattern.write_patterns_file(
|
|
||||||
tuple(
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
|
||||||
special_file_path,
|
|
||||||
borgmatic.borg.pattern.Pattern_type.NO_RECURSE,
|
|
||||||
borgmatic.borg.pattern.Pattern_style.FNMATCH,
|
|
||||||
source=borgmatic.borg.pattern.Pattern_source.INTERNAL,
|
|
||||||
)
|
|
||||||
for special_file_path in special_file_paths
|
|
||||||
),
|
|
||||||
borgmatic_runtime_directory,
|
|
||||||
patterns_file=patterns_file,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if '--patterns-from' not in create_flags:
|
if special_file_paths:
|
||||||
create_flags += ('--patterns-from', patterns_file.name)
|
truncated_special_file_paths = textwrap.shorten(
|
||||||
|
', '.join(special_file_paths),
|
||||||
|
width=MAX_SPECIAL_FILE_PATHS_LENGTH,
|
||||||
|
placeholder=' ...',
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
f'Excluding special files to prevent Borg from hanging: {truncated_special_file_paths}',
|
||||||
|
)
|
||||||
|
patterns_file = borgmatic.borg.pattern.write_patterns_file(
|
||||||
|
tuple(
|
||||||
|
borgmatic.borg.pattern.Pattern(
|
||||||
|
special_file_path,
|
||||||
|
borgmatic.borg.pattern.Pattern_type.NO_RECURSE,
|
||||||
|
borgmatic.borg.pattern.Pattern_style.FNMATCH,
|
||||||
|
source=borgmatic.borg.pattern.Pattern_source.INTERNAL,
|
||||||
|
)
|
||||||
|
for special_file_path in special_file_paths
|
||||||
|
),
|
||||||
|
borgmatic_runtime_directory,
|
||||||
|
patterns_file=patterns_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
if '--patterns-from' not in create_flags:
|
||||||
|
create_flags += ('--patterns-from', patterns_file.name)
|
||||||
|
|
||||||
return (create_flags, create_positional_arguments, patterns_file)
|
return (create_flags, create_positional_arguments, patterns_file)
|
||||||
|
|
||||||
@@ -393,31 +352,25 @@ def create_archive(
|
|||||||
borg_exit_codes = config.get('borg_exit_codes')
|
borg_exit_codes = config.get('borg_exit_codes')
|
||||||
|
|
||||||
if stream_processes:
|
if stream_processes:
|
||||||
output = '\n'.join(
|
return execute_command_with_processes(
|
||||||
execute_command_with_processes(
|
create_flags + create_positional_arguments,
|
||||||
create_flags + create_positional_arguments,
|
stream_processes,
|
||||||
stream_processes,
|
output_log_level,
|
||||||
output_log_level,
|
output_file,
|
||||||
output_file,
|
working_directory=working_directory,
|
||||||
working_directory=working_directory,
|
environment=environment.make_environment(config),
|
||||||
environment=environment.make_environment(config),
|
borg_local_path=local_path,
|
||||||
borg_local_path=local_path,
|
borg_exit_codes=borg_exit_codes,
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return output if json else None
|
|
||||||
|
|
||||||
if output_log_level is None:
|
if output_log_level is None:
|
||||||
output = '\n'.join(
|
return execute_command_and_capture_output(
|
||||||
execute_command_and_capture_output(
|
create_flags + create_positional_arguments,
|
||||||
create_flags + create_positional_arguments,
|
working_directory=working_directory,
|
||||||
working_directory=working_directory,
|
environment=environment.make_environment(config),
|
||||||
environment=environment.make_environment(config),
|
borg_local_path=local_path,
|
||||||
borg_local_path=local_path,
|
borg_exit_codes=borg_exit_codes,
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return output if json else None
|
|
||||||
|
|
||||||
execute_command(
|
execute_command(
|
||||||
create_flags + create_positional_arguments,
|
create_flags + create_positional_arguments,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.borg.environment
|
import borgmatic.borg.environment
|
||||||
import borgmatic.borg.feature
|
import borgmatic.borg.feature
|
||||||
@@ -29,8 +28,6 @@ def make_delete_command(
|
|||||||
arguments to the delete action as an argparse.Namespace, and global arguments, return a command
|
arguments to the delete action as an argparse.Namespace, and global arguments, return a command
|
||||||
as a tuple to delete archives from the repository.
|
as a tuple to delete archives from the repository.
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('delete', '')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(local_path, 'delete')
|
(local_path, 'delete')
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
@@ -38,7 +35,7 @@ def make_delete_command(
|
|||||||
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
|
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
|
||||||
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
|
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
|
||||||
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
|
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
|
||||||
+ ('--log-json',)
|
+ borgmatic.borg.flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||||
+ borgmatic.borg.flags.make_flags('list', config.get('list_details'))
|
+ borgmatic.borg.flags.make_flags('list', config.get('list_details'))
|
||||||
+ (
|
+ (
|
||||||
@@ -69,7 +66,6 @@ def make_delete_command(
|
|||||||
'repository',
|
'repository',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
|
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import logging
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.borg.environment
|
|
||||||
import borgmatic.borg.feature
|
|
||||||
import borgmatic.config.paths
|
|
||||||
import borgmatic.execute
|
|
||||||
from borgmatic.borg import flags
|
|
||||||
from borgmatic.borg.pattern import write_patterns_file
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def diff(
|
|
||||||
repository,
|
|
||||||
archive,
|
|
||||||
second_archive,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
diff_arguments,
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path=None,
|
|
||||||
patterns=None,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Given a local or remote repository path, two archive names, a configuration dict, the local Borg
|
|
||||||
version string, an argparse.Namespace of diff arguments, an argparse.Namespace of global
|
|
||||||
arguments, optional local and remote Borg paths, executes the diff command with the given
|
|
||||||
arguments.
|
|
||||||
'''
|
|
||||||
borgmatic.logger.add_custom_log_levels()
|
|
||||||
|
|
||||||
lock_wait = config.get('lock_wait', None)
|
|
||||||
exclude_flags = flags.make_exclude_flags(config)
|
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('diff', '')
|
|
||||||
|
|
||||||
if diff_arguments.only_patterns:
|
|
||||||
# Write patterns to a temporary file and use that file with --patterns-from.
|
|
||||||
patterns_file = write_patterns_file(
|
|
||||||
patterns,
|
|
||||||
borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
patterns_file = None
|
|
||||||
|
|
||||||
if borgmatic.borg.feature.available(
|
|
||||||
borgmatic.borg.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 ()
|
|
||||||
|
|
||||||
diff_command = (
|
|
||||||
(local_path, 'diff')
|
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
|
||||||
+ ('--log-json',)
|
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait is not None else ())
|
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
|
||||||
+ (
|
|
||||||
('--patterns-from', patterns_file.name)
|
|
||||||
if patterns_file and diff_arguments.only_patterns
|
|
||||||
else ()
|
|
||||||
)
|
|
||||||
+ exclude_flags
|
|
||||||
+ numeric_ids_flags
|
|
||||||
+ (('--same-chunker-params',) if diff_arguments.same_chunker_params else ())
|
|
||||||
+ (('--sort-by', ','.join(diff_arguments.sort_keys)) if diff_arguments.sort_keys else ())
|
|
||||||
+ (('--content-only',) if diff_arguments.content_only else ())
|
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ (
|
|
||||||
(*flags.make_repository_flags(repository, local_borg_version), archive)
|
|
||||||
if borgmatic.borg.feature.available(
|
|
||||||
borgmatic.borg.feature.Feature.SEPARATE_REPOSITORY_ARCHIVE,
|
|
||||||
local_borg_version,
|
|
||||||
)
|
|
||||||
else flags.make_repository_archive_flags(repository, archive, local_borg_version)
|
|
||||||
)
|
|
||||||
+ (second_archive,)
|
|
||||||
)
|
|
||||||
|
|
||||||
borgmatic.execute.execute_command(
|
|
||||||
full_command=diff_command,
|
|
||||||
output_log_level=logging.ANSWER,
|
|
||||||
environment=borgmatic.borg.environment.make_environment(config),
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=config.get('borg_exit_codes'),
|
|
||||||
)
|
|
||||||
@@ -10,7 +10,6 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
|
|||||||
'borg_files_cache_ttl': 'BORG_FILES_CACHE_TTL',
|
'borg_files_cache_ttl': 'BORG_FILES_CACHE_TTL',
|
||||||
'borg_security_directory': 'BORG_SECURITY_DIR',
|
'borg_security_directory': 'BORG_SECURITY_DIR',
|
||||||
'borg_keys_directory': 'BORG_KEYS_DIR',
|
'borg_keys_directory': 'BORG_KEYS_DIR',
|
||||||
'borg_key_file': 'BORG_KEY_FILE',
|
|
||||||
'ssh_command': 'BORG_RSH',
|
'ssh_command': 'BORG_RSH',
|
||||||
'temporary_directory': 'TMPDIR',
|
'temporary_directory': 'TMPDIR',
|
||||||
}
|
}
|
||||||
@@ -25,7 +24,6 @@ DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
|
|||||||
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
|
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
|
||||||
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
|
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
|
||||||
'use_chunks_archive': 'BORG_USE_CHUNKS_ARCHIVE',
|
'use_chunks_archive': 'BORG_USE_CHUNKS_ARCHIVE',
|
||||||
'msgpack_version_check': 'BORG_MSGPACK_VERSION_CHECK',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -91,8 +89,7 @@ def make_environment(config):
|
|||||||
) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items():
|
) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items():
|
||||||
if os.environ.get(environment_variable_name) is None:
|
if os.environ.get(environment_variable_name) is None:
|
||||||
value = config.get(option_name)
|
value = config.get(option_name)
|
||||||
if value is not None:
|
environment[environment_variable_name] = 'YES' if value else 'NO'
|
||||||
environment[environment_variable_name] = 'YES' if value else 'NO'
|
|
||||||
|
|
||||||
for (
|
for (
|
||||||
option_name,
|
option_name,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
@@ -32,7 +31,6 @@ def export_key(
|
|||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('key_export', '')
|
|
||||||
|
|
||||||
if export_arguments.path and export_arguments.path != '-':
|
if export_arguments.path and export_arguments.path != '-':
|
||||||
if os.path.exists(os.path.join(working_directory or '', export_arguments.path)):
|
if os.path.exists(os.path.join(working_directory or '', export_arguments.path)):
|
||||||
@@ -48,13 +46,12 @@ def export_key(
|
|||||||
(local_path, 'key', 'export')
|
(local_path, 'key', 'export')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (('--log-json',) if output_file is None else ())
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ flags.make_flags('paper', export_arguments.paper)
|
+ flags.make_flags('paper', export_arguments.paper)
|
||||||
+ flags.make_flags('qr-html', export_arguments.qr_html)
|
+ flags.make_flags('qr-html', export_arguments.qr_html)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(
|
+ flags.make_repository_flags(
|
||||||
repository_path,
|
repository_path,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
@@ -35,13 +34,12 @@ def export_tar_archive(
|
|||||||
borgmatic.logger.add_custom_log_levels()
|
borgmatic.logger.add_custom_log_levels()
|
||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('export_tar', '')
|
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'export-tar')
|
(local_path, 'export-tar')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (('--log-json',) if destination_path != '-' else ())
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--list',) if config.get('list_details') else ())
|
+ (('--list',) if config.get('list_details') else ())
|
||||||
@@ -49,7 +47,6 @@ def export_tar_archive(
|
|||||||
+ (('--dry-run',) if dry_run else ())
|
+ (('--dry-run',) if dry_run else ())
|
||||||
+ (('--tar-filter', tar_filter) if tar_filter else ())
|
+ (('--tar-filter', tar_filter) if tar_filter else ())
|
||||||
+ (('--strip-components', str(strip_components)) if strip_components else ())
|
+ (('--strip-components', str(strip_components)) if strip_components else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_archive_flags(
|
+ flags.make_repository_archive_flags(
|
||||||
repository_path,
|
repository_path,
|
||||||
archive,
|
archive,
|
||||||
|
|||||||
+24
-37
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shlex
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
@@ -24,7 +23,6 @@ def extract_last_archive_dry_run(
|
|||||||
Perform an extraction dry-run of the most recent archive. If there are no archives, skip the
|
Perform an extraction dry-run of the most recent archive. If there are no archives, skip the
|
||||||
dry-run.
|
dry-run.
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('extract', '')
|
|
||||||
verbosity_flags = ()
|
verbosity_flags = ()
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
verbosity_flags = ('--debug', '--show-rc')
|
verbosity_flags = ('--debug', '--show-rc')
|
||||||
@@ -49,12 +47,10 @@ def extract_last_archive_dry_run(
|
|||||||
full_extract_command = (
|
full_extract_command = (
|
||||||
(local_path, 'extract', '--dry-run')
|
(local_path, 'extract', '--dry-run')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--log-json',) if not config.get('progress') else ())
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ verbosity_flags
|
+ verbosity_flags
|
||||||
+ (('--progress',) if config.get('progress') else ())
|
|
||||||
+ list_flag
|
+ list_flag
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_archive_flags(
|
+ flags.make_repository_archive_flags(
|
||||||
repository_path,
|
repository_path,
|
||||||
last_archive_name,
|
last_archive_name,
|
||||||
@@ -62,23 +58,13 @@ def extract_last_archive_dry_run(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.get('progress'):
|
execute_command(
|
||||||
execute_command(
|
full_extract_command,
|
||||||
full_extract_command,
|
environment=environment.make_environment(config),
|
||||||
output_file=DO_NOT_CAPTURE,
|
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||||
environment=environment.make_environment(config),
|
borg_local_path=local_path,
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
borg_exit_codes=config.get('borg_exit_codes'),
|
||||||
borg_local_path=local_path,
|
)
|
||||||
borg_exit_codes=config.get('borg_exit_codes'),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
execute_command(
|
|
||||||
full_extract_command,
|
|
||||||
environment=environment.make_environment(config),
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=config.get('borg_exit_codes'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_archive(
|
def extract_archive(
|
||||||
@@ -106,7 +92,9 @@ def extract_archive(
|
|||||||
'''
|
'''
|
||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('extract', '')
|
|
||||||
|
if config.get('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):
|
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
|
||||||
numeric_ids_flags = ('--numeric-ids',) if config.get('numeric_ids') else ()
|
numeric_ids_flags = ('--numeric-ids',) if config.get('numeric_ids') else ()
|
||||||
@@ -135,15 +123,14 @@ def extract_archive(
|
|||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ numeric_ids_flags
|
+ numeric_ids_flags
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (('--log-json',) if (config.get('log_json') or not config.get('progress')) else ())
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ (('--dry-run',) if dry_run else ())
|
+ (('--dry-run',) if dry_run else ())
|
||||||
+ (('--strip-components', str(strip_components)) if strip_components else ())
|
+ (('--strip-components', str(strip_components)) if strip_components else ())
|
||||||
+ (('--progress',) if config.get('progress') and not extract_to_stdout else ())
|
+ (('--progress',) if config.get('progress') else ())
|
||||||
+ (('--stdout',) if extract_to_stdout else ())
|
+ (('--stdout',) if extract_to_stdout else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_archive_flags(
|
+ flags.make_repository_archive_flags(
|
||||||
# Make the repository path absolute so the destination directory used below via changing
|
# Make the repository path absolute so the destination directory used below via changing
|
||||||
# the working directory doesn't prevent Borg from finding the repo. But also apply the
|
# the working directory doesn't prevent Borg from finding the repo. But also apply the
|
||||||
@@ -160,17 +147,6 @@ def extract_archive(
|
|||||||
os.path.join(working_directory or '', destination_path) if destination_path else None
|
os.path.join(working_directory or '', destination_path) if destination_path else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if extract_to_stdout:
|
|
||||||
return execute_command(
|
|
||||||
full_command,
|
|
||||||
output_file=subprocess.PIPE,
|
|
||||||
run_to_completion=False,
|
|
||||||
environment=environment.make_environment(config),
|
|
||||||
working_directory=full_destination_path,
|
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
|
|
||||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||||
# the terminal directly.
|
# the terminal directly.
|
||||||
if config.get('progress'):
|
if config.get('progress'):
|
||||||
@@ -184,6 +160,17 @@ def extract_archive(
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if extract_to_stdout:
|
||||||
|
return execute_command(
|
||||||
|
full_command,
|
||||||
|
output_file=subprocess.PIPE,
|
||||||
|
run_to_completion=False,
|
||||||
|
environment=environment.make_environment(config),
|
||||||
|
working_directory=full_destination_path,
|
||||||
|
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
|
# 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.
|
# if the restore paths don't exist in the archive.
|
||||||
execute_command(
|
execute_command(
|
||||||
|
|||||||
+20
-10
@@ -1,4 +1,5 @@
|
|||||||
import itertools
|
import itertools
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -134,18 +135,27 @@ def make_match_archives_flags( # noqa: PLR0911
|
|||||||
return ('--glob-archives', f'{derived_match_archives}')
|
return ('--glob-archives', f'{derived_match_archives}')
|
||||||
|
|
||||||
|
|
||||||
def warn_for_aggressive_archive_flags(command, output_lines):
|
def warn_for_aggressive_archive_flags(json_command, json_output):
|
||||||
'''
|
'''
|
||||||
Given an archives command and the resulting output lines from running it, warn if the command
|
Given a JSON archives command and the resulting JSON string output from running it, parse the
|
||||||
used an archive flag but the output indicates zero archives were found.
|
JSON and warn if the command used an archive flag but the output indicates zero archives were
|
||||||
|
found.
|
||||||
'''
|
'''
|
||||||
if {'--glob-archives', '--match-archives'}.intersection(set(command)) and len(
|
archive_flags_used = {'--glob-archives', '--match-archives'}.intersection(set(json_command))
|
||||||
tuple(line for line in output_lines if not line.startswith('terminating with '))
|
|
||||||
) == 0:
|
if not archive_flags_used:
|
||||||
logger.warning('An archive filter was applied, but no matching archives were found.')
|
return
|
||||||
logger.warning(
|
|
||||||
'Try adding --match-archives "*" or adjusting archive_name_format/match_archives in configuration.',
|
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')
|
||||||
|
|
||||||
|
|
||||||
def omit_flag(arguments, flag):
|
def omit_flag(arguments, flag):
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, flags
|
from borgmatic.borg import environment, flags
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -30,30 +30,29 @@ def import_key(
|
|||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('key_import', '')
|
|
||||||
|
|
||||||
if (
|
if import_arguments.path and import_arguments.path != '-':
|
||||||
import_arguments.path
|
if not os.path.exists(os.path.join(working_directory or '', import_arguments.path)):
|
||||||
and import_arguments.path != '-'
|
raise ValueError(f'Path {import_arguments.path} does not exist. Aborting.')
|
||||||
and not os.path.exists(os.path.join(working_directory or '', import_arguments.path))
|
|
||||||
):
|
input_file = None
|
||||||
raise ValueError(f'Path {import_arguments.path} does not exist. Aborting.')
|
else:
|
||||||
|
input_file = DO_NOT_CAPTURE
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'key', 'import')
|
(local_path, 'key', 'import')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ ('--log-json',)
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ flags.make_flags('paper', import_arguments.paper)
|
+ flags.make_flags('paper', import_arguments.paper)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(
|
+ flags.make_repository_flags(
|
||||||
repository_path,
|
repository_path,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
)
|
)
|
||||||
+ (import_arguments.path or '-',)
|
+ ((import_arguments.path,) if input_file is None else ())
|
||||||
)
|
)
|
||||||
|
|
||||||
if global_arguments.dry_run:
|
if global_arguments.dry_run:
|
||||||
@@ -62,6 +61,7 @@ def import_key(
|
|||||||
|
|
||||||
execute_command(
|
execute_command(
|
||||||
full_command,
|
full_command,
|
||||||
|
input_file=input_file,
|
||||||
output_log_level=logging.INFO,
|
output_log_level=logging.INFO,
|
||||||
environment=environment.make_environment(config),
|
environment=environment.make_environment(config),
|
||||||
working_directory=working_directory,
|
working_directory=working_directory,
|
||||||
|
|||||||
+21
-27
@@ -1,11 +1,10 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags
|
||||||
from borgmatic.execute import execute_command_and_capture_output
|
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -24,8 +23,6 @@ def make_info_command(
|
|||||||
arguments to the info action as an argparse.Namespace, and global arguments, return a command
|
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.
|
as a tuple to display summary information for archives in the repository.
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('info', '')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(local_path, 'info')
|
(local_path, 'info')
|
||||||
+ (
|
+ (
|
||||||
@@ -40,7 +37,7 @@ def make_info_command(
|
|||||||
)
|
)
|
||||||
+ flags.make_flags('remote-path', remote_path)
|
+ flags.make_flags('remote-path', remote_path)
|
||||||
+ flags.make_flags('umask', config.get('umask'))
|
+ flags.make_flags('umask', config.get('umask'))
|
||||||
+ ('--log-json',)
|
+ flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||||
+ (
|
+ (
|
||||||
(
|
(
|
||||||
@@ -61,7 +58,6 @@ def make_info_command(
|
|||||||
info_arguments,
|
info_arguments,
|
||||||
excludes=('repository', 'archive', 'prefix', 'match_archives'),
|
excludes=('repository', 'archive', 'prefix', 'match_archives'),
|
||||||
)
|
)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,28 +99,26 @@ def display_archives_info(
|
|||||||
borg_exit_codes = config.get('borg_exit_codes')
|
borg_exit_codes = config.get('borg_exit_codes')
|
||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
|
|
||||||
if info_arguments.json:
|
json_info = execute_command_and_capture_output(
|
||||||
return '\n'.join(
|
json_command,
|
||||||
execute_command_and_capture_output(
|
environment=environment.make_environment(config),
|
||||||
json_command,
|
working_directory=working_directory,
|
||||||
environment=environment.make_environment(config),
|
borg_local_path=local_path,
|
||||||
working_directory=working_directory,
|
borg_exit_codes=borg_exit_codes,
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
output_lines = tuple(
|
|
||||||
execute_command_and_capture_output(
|
|
||||||
main_command,
|
|
||||||
output_log_level=logging.ANSWER,
|
|
||||||
environment=environment.make_environment(config),
|
|
||||||
working_directory=working_directory,
|
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
flags.warn_for_aggressive_archive_flags(main_command, output_lines)
|
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,
|
||||||
|
environment=environment.make_environment(config),
|
||||||
|
working_directory=working_directory,
|
||||||
|
borg_local_path=local_path,
|
||||||
|
borg_exit_codes=borg_exit_codes,
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
+34
-33
@@ -1,9 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import copy
|
import copy
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
@@ -19,8 +17,6 @@ MAKE_FLAGS_EXCLUDES = (
|
|||||||
'archive',
|
'archive',
|
||||||
'paths',
|
'paths',
|
||||||
'find_paths',
|
'find_paths',
|
||||||
'format',
|
|
||||||
'json',
|
|
||||||
*ARCHIVE_FILTER_FLAGS_MOVED_TO_REPO_LIST,
|
*ARCHIVE_FILTER_FLAGS_MOVED_TO_REPO_LIST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,8 +35,6 @@ def make_list_command(
|
|||||||
and local and remote Borg paths, return a command as a tuple to list archives or paths within an
|
and local and remote Borg paths, return a command as a tuple to list archives or paths within an
|
||||||
archive.
|
archive.
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('list', '')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(local_path, 'list')
|
(local_path, 'list')
|
||||||
+ (
|
+ (
|
||||||
@@ -55,12 +49,9 @@ def make_list_command(
|
|||||||
)
|
)
|
||||||
+ flags.make_flags('remote-path', remote_path)
|
+ flags.make_flags('remote-path', remote_path)
|
||||||
+ flags.make_flags('umask', config.get('umask'))
|
+ flags.make_flags('umask', config.get('umask'))
|
||||||
+ ('--log-json',)
|
+ flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ flags.make_flags('json-lines', list_arguments.json)
|
|
||||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||||
+ flags.make_flags('format', list_arguments.format or config.get('file_list_format'))
|
|
||||||
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
|
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ (
|
+ (
|
||||||
flags.make_repository_archive_flags(
|
flags.make_repository_archive_flags(
|
||||||
repository_path,
|
repository_path,
|
||||||
@@ -112,15 +103,14 @@ def capture_archive_listing(
|
|||||||
remote_path=None,
|
remote_path=None,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Given a local or remote repository path, an archive name, a configuration dict, the local Borg
|
Given a local or remote repository path, an archive name, a configuration
|
||||||
version, global arguments as an argparse.Namespace, the archive paths (or Borg patterns) in
|
dict, the local Borg version, global arguments as an argparse.Namespace,
|
||||||
which to list files, the Borg path format indicating keys to include in the output, and local
|
the archive paths (or Borg patterns) in which to list files, the Borg path
|
||||||
and remote Borg paths, capture the output of listing that archive and return it as a sequence of
|
format to use for the output, and local and remote Borg paths, capture the
|
||||||
dicts, one per path.
|
output of listing that archive and return it as a list of file paths.
|
||||||
'''
|
'''
|
||||||
return tuple(
|
return tuple(
|
||||||
json.loads(entry)
|
execute_command_and_capture_output(
|
||||||
for entry in execute_command_and_capture_output(
|
|
||||||
make_list_command(
|
make_list_command(
|
||||||
repository_path,
|
repository_path,
|
||||||
config,
|
config,
|
||||||
@@ -130,8 +120,8 @@ def capture_archive_listing(
|
|||||||
archive=archive,
|
archive=archive,
|
||||||
paths=list(list_paths) if list_paths else None,
|
paths=list(list_paths) if list_paths else None,
|
||||||
find_paths=None,
|
find_paths=None,
|
||||||
json=True,
|
json=None,
|
||||||
format=path_format or None,
|
format=path_format or '{path}{NUL}',
|
||||||
),
|
),
|
||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
@@ -142,6 +132,8 @@ def capture_archive_listing(
|
|||||||
borg_local_path=local_path,
|
borg_local_path=local_path,
|
||||||
borg_exit_codes=config.get('borg_exit_codes'),
|
borg_exit_codes=config.get('borg_exit_codes'),
|
||||||
)
|
)
|
||||||
|
.strip('\0')
|
||||||
|
.split('\0'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -198,6 +190,11 @@ def list_archive(
|
|||||||
f"The --{name.replace('_', '-')} flag on the list action is ignored when using the --archive flag.",
|
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_exit_codes = config.get('borg_exit_codes')
|
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
|
# If there are any paths to find (and there's not a single archive already selected), start by
|
||||||
@@ -216,20 +213,24 @@ def list_archive(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Ask Borg to list archives. Capture its output for use below.
|
# Ask Borg to list archives. Capture its output for use below.
|
||||||
archive_lines = execute_command_and_capture_output(
|
archive_lines = tuple(
|
||||||
repo_list.make_repo_list_command(
|
execute_command_and_capture_output(
|
||||||
repository_path,
|
repo_list.make_repo_list_command(
|
||||||
config,
|
repository_path,
|
||||||
local_borg_version,
|
config,
|
||||||
repo_list_arguments,
|
local_borg_version,
|
||||||
global_arguments,
|
repo_list_arguments,
|
||||||
local_path,
|
global_arguments,
|
||||||
remote_path,
|
local_path,
|
||||||
),
|
remote_path,
|
||||||
environment=environment.make_environment(config),
|
),
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
environment=environment.make_environment(config),
|
||||||
borg_local_path=local_path,
|
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||||
borg_exit_codes=borg_exit_codes,
|
borg_local_path=local_path,
|
||||||
|
borg_exit_codes=borg_exit_codes,
|
||||||
|
)
|
||||||
|
.strip('\n')
|
||||||
|
.splitlines(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
archive_lines = (list_arguments.archive,)
|
archive_lines = (list_arguments.archive,)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags
|
||||||
@@ -26,13 +25,12 @@ def mount_archive(
|
|||||||
'''
|
'''
|
||||||
umask = config.get('umask', None)
|
umask = config.get('umask', None)
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('mount', '')
|
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'mount')
|
(local_path, 'mount')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (('--log-json',) if not mount_arguments.foreground else ())
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
@@ -41,7 +39,6 @@ def mount_archive(
|
|||||||
excludes=('repository', 'archive', 'mount_point', 'paths', 'options'),
|
excludes=('repository', 'archive', 'mount_point', 'paths', 'options'),
|
||||||
)
|
)
|
||||||
+ (('-o', mount_arguments.options) if mount_arguments.options else ())
|
+ (('-o', mount_arguments.options) if mount_arguments.options else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ (
|
+ (
|
||||||
(
|
(
|
||||||
flags.make_repository_flags(repository_path, local_borg_version)
|
flags.make_repository_flags(repository_path, local_borg_version)
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ def run_passcommand(passcommand, working_directory):
|
|||||||
Cache the results so that the passcommand only needs to run—and potentially prompt the user—once
|
Cache the results so that the passcommand only needs to run—and potentially prompt the user—once
|
||||||
per borgmatic invocation.
|
per borgmatic invocation.
|
||||||
'''
|
'''
|
||||||
return '\n'.join(
|
return borgmatic.execute.execute_command_and_capture_output(
|
||||||
borgmatic.execute.execute_command_and_capture_output(
|
shlex.split(passcommand),
|
||||||
shlex.split(passcommand),
|
working_directory=working_directory,
|
||||||
working_directory=working_directory,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -88,16 +88,16 @@ def write_patterns_file(patterns, borgmatic_runtime_directory, patterns_file=Non
|
|||||||
return patterns_file
|
return patterns_file
|
||||||
|
|
||||||
|
|
||||||
def check_all_root_patterns_exist(patterns, working_directory):
|
def check_all_root_patterns_exist(patterns):
|
||||||
'''
|
'''
|
||||||
Given a sequence of Pattern instances and the current working directory, check that all root
|
Given a sequence of Pattern instances, check that all root pattern paths exist. If any don't,
|
||||||
pattern paths exist. If any don't, raise an exception.
|
raise an exception.
|
||||||
'''
|
'''
|
||||||
missing_paths = [
|
missing_paths = [
|
||||||
pattern.path
|
pattern.path
|
||||||
for pattern in patterns
|
for pattern in patterns
|
||||||
if pattern.type == Pattern_type.ROOT
|
if pattern.type == Pattern_type.ROOT
|
||||||
if not os.path.exists(os.path.join(working_directory or '', pattern.path))
|
if not os.path.exists(pattern.path)
|
||||||
]
|
]
|
||||||
|
|
||||||
if missing_paths:
|
if missing_paths:
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ def prune_archives(
|
|||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'prune')
|
(local_path, 'prune')
|
||||||
+ make_prune_flags(config, prune_arguments, local_borg_version)
|
+ make_prune_flags(config, prune_arguments, local_borg_version)
|
||||||
+ ('--log-json',)
|
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (
|
+ (
|
||||||
('--stats',)
|
('--stats',)
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ def recreate_archive(
|
|||||||
exclude_flags = flags.make_exclude_flags(config)
|
exclude_flags = flags.make_exclude_flags(config)
|
||||||
compression = config.get('compression', None)
|
compression = config.get('compression', None)
|
||||||
chunker_params = config.get('chunker_params', None)
|
chunker_params = config.get('chunker_params', None)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('recreate', '')
|
|
||||||
|
|
||||||
# Available recompress MODES: "if-different", "always", "never" (default)
|
# Available recompress MODES: "if-different", "always", "never" (default)
|
||||||
recompress = config.get('recompress', None)
|
recompress = config.get('recompress', None)
|
||||||
@@ -46,7 +45,7 @@ def recreate_archive(
|
|||||||
recreate_command = (
|
recreate_command = (
|
||||||
(local_path, 'recreate')
|
(local_path, 'recreate')
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ ('--log-json',)
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait is not None else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait is not None else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
@@ -72,8 +71,6 @@ def recreate_archive(
|
|||||||
+ (('--chunker-params', chunker_params) if chunker_params else ())
|
+ (('--chunker-params', chunker_params) if chunker_params else ())
|
||||||
+ (('--recompress', recompress) if recompress else ())
|
+ (('--recompress', recompress) if recompress else ())
|
||||||
+ exclude_flags
|
+ exclude_flags
|
||||||
+ (('--dry-run',) if global_arguments.dry_run else ())
|
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ (
|
+ (
|
||||||
(
|
(
|
||||||
flags.make_repository_flags(repository, local_borg_version)
|
flags.make_repository_flags(repository, local_borg_version)
|
||||||
@@ -95,6 +92,10 @@ def recreate_archive(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if global_arguments.dry_run:
|
||||||
|
logger.info('Skipping the archive recreation (dry run)')
|
||||||
|
return
|
||||||
|
|
||||||
borgmatic.execute.execute_command(
|
borgmatic.execute.execute_command(
|
||||||
full_command=recreate_command,
|
full_command=recreate_command,
|
||||||
output_log_level=logging.INFO,
|
output_log_level=logging.INFO,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.borg.environment
|
|
||||||
import borgmatic.borg.flags
|
import borgmatic.borg.flags
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -17,8 +15,6 @@ def make_rename_command(
|
|||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
):
|
):
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('rename', '')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(local_path, 'rename')
|
(local_path, 'rename')
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
@@ -26,9 +22,8 @@ def make_rename_command(
|
|||||||
+ borgmatic.borg.flags.make_flags('dry-run', dry_run)
|
+ borgmatic.borg.flags.make_flags('dry-run', dry_run)
|
||||||
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
|
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
|
||||||
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
|
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
|
||||||
+ ('--log-json',)
|
+ borgmatic.borg.flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ borgmatic.borg.flags.make_repository_archive_flags(
|
+ borgmatic.borg.flags.make_repository_archive_flags(
|
||||||
repository_name,
|
repository_name,
|
||||||
old_archive_name,
|
old_archive_name,
|
||||||
|
|||||||
@@ -66,14 +66,8 @@ def create_repository(
|
|||||||
|
|
||||||
lock_wait = config.get('lock_wait')
|
lock_wait = config.get('lock_wait')
|
||||||
umask = config.get('umask')
|
umask = config.get('umask')
|
||||||
extra_borg_options_from_init = config.get('extra_borg_options', {}).get('init', '')
|
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('repo-create', '')
|
extra_borg_options = config.get('extra_borg_options', {}).get('repo-create', '')
|
||||||
|
|
||||||
if extra_borg_options_from_init:
|
|
||||||
logger.warning(
|
|
||||||
'The "init" option in "extra_borg_options" is deprecated and will be removed from a future release; use "repo_create" instead.'
|
|
||||||
)
|
|
||||||
|
|
||||||
repo_create_command = (
|
repo_create_command = (
|
||||||
(local_path,)
|
(local_path,)
|
||||||
+ (
|
+ (
|
||||||
@@ -89,11 +83,11 @@ def create_repository(
|
|||||||
+ (('--make-parent-dirs',) if make_parent_directories else ())
|
+ (('--make-parent-dirs',) if make_parent_directories else ())
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
|
+ (('--log-json',) if config.get('log_json') else ())
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
+ (('--umask', str(umask)) if umask else ())
|
+ (('--umask', str(umask)) if umask else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options_from_init)) if extra_borg_options_from_init else ())
|
|
||||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.borg.environment
|
import borgmatic.borg.environment
|
||||||
import borgmatic.borg.feature
|
import borgmatic.borg.feature
|
||||||
@@ -21,23 +20,12 @@ def make_repo_delete_command(
|
|||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
output_file,
|
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Given a local or remote repository dict, a configuration dict, the local Borg version, the
|
Given a local or remote repository dict, a configuration dict, the local Borg version, the
|
||||||
arguments to the repo_delete action as an argparse.Namespace, and global arguments, the Borg
|
arguments to the repo_delete action as an argparse.Namespace, and global arguments, return a command
|
||||||
local path, the Borg remote path, and an optional output file, return a command as a tuple to
|
as a tuple to repo_delete the entire repository.
|
||||||
repo_delete the entire repository.
|
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get(
|
|
||||||
'repo_delete'
|
|
||||||
if borgmatic.borg.feature.available(
|
|
||||||
borgmatic.borg.feature.Feature.REPO_DELETE, local_borg_version
|
|
||||||
)
|
|
||||||
else 'delete',
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(local_path,)
|
(local_path,)
|
||||||
+ (
|
+ (
|
||||||
@@ -53,7 +41,7 @@ def make_repo_delete_command(
|
|||||||
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
|
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
|
||||||
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
|
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
|
||||||
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
|
+ borgmatic.borg.flags.make_flags('umask', config.get('umask'))
|
||||||
+ (('--log-json',) if output_file is None else ())
|
+ borgmatic.borg.flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||||
+ borgmatic.borg.flags.make_flags('list', config.get('list_details'))
|
+ borgmatic.borg.flags.make_flags('list', config.get('list_details'))
|
||||||
+ (
|
+ (
|
||||||
@@ -68,7 +56,6 @@ def make_repo_delete_command(
|
|||||||
repo_delete_arguments,
|
repo_delete_arguments,
|
||||||
excludes=('list_details', 'force', 'repository'),
|
excludes=('list_details', 'force', 'repository'),
|
||||||
)
|
)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
|
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,14 +76,6 @@ def delete_repository(
|
|||||||
'''
|
'''
|
||||||
borgmatic.logger.add_custom_log_levels()
|
borgmatic.logger.add_custom_log_levels()
|
||||||
|
|
||||||
# Don't capture output when Borg is expected to prompt for interactive confirmation, or the
|
|
||||||
# prompt won't work.
|
|
||||||
output_file = (
|
|
||||||
None
|
|
||||||
if repo_delete_arguments.force or repo_delete_arguments.cache_only
|
|
||||||
else borgmatic.execute.DO_NOT_CAPTURE
|
|
||||||
)
|
|
||||||
|
|
||||||
command = make_repo_delete_command(
|
command = make_repo_delete_command(
|
||||||
repository,
|
repository,
|
||||||
config,
|
config,
|
||||||
@@ -105,13 +84,18 @@ def delete_repository(
|
|||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
output_file,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
borgmatic.execute.execute_command(
|
borgmatic.execute.execute_command(
|
||||||
command,
|
command,
|
||||||
output_log_level=logging.ANSWER,
|
output_log_level=logging.ANSWER,
|
||||||
output_file=output_file,
|
# Don't capture output when Borg is expected to prompt for interactive confirmation, or the
|
||||||
|
# prompt won't work.
|
||||||
|
output_file=(
|
||||||
|
None
|
||||||
|
if repo_delete_arguments.force or repo_delete_arguments.cache_only
|
||||||
|
else borgmatic.execute.DO_NOT_CAPTURE
|
||||||
|
),
|
||||||
environment=borgmatic.borg.environment.make_environment(config),
|
environment=borgmatic.borg.environment.make_environment(config),
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||||
borg_local_path=local_path,
|
borg_local_path=local_path,
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import argparse
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
@@ -27,10 +24,6 @@ def display_repository_info(
|
|||||||
'''
|
'''
|
||||||
borgmatic.logger.add_custom_log_levels()
|
borgmatic.logger.add_custom_log_levels()
|
||||||
lock_wait = config.get('lock_wait', None)
|
lock_wait = config.get('lock_wait', None)
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get(
|
|
||||||
'repo_info' if feature.available(feature.Feature.REPO_INFO, local_borg_version) else 'info',
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path,)
|
(local_path,)
|
||||||
@@ -39,7 +32,6 @@ def display_repository_info(
|
|||||||
if feature.available(feature.Feature.REPO_INFO, local_borg_version)
|
if feature.available(feature.Feature.REPO_INFO, local_borg_version)
|
||||||
else ('info',)
|
else ('info',)
|
||||||
)
|
)
|
||||||
+ (('--critical',) if repo_info_arguments.json else ())
|
|
||||||
+ (
|
+ (
|
||||||
('--info',)
|
('--info',)
|
||||||
if logger.getEffectiveLevel() == logging.INFO and not repo_info_arguments.json
|
if logger.getEffectiveLevel() == logging.INFO and not repo_info_arguments.json
|
||||||
@@ -52,10 +44,9 @@ def display_repository_info(
|
|||||||
)
|
)
|
||||||
+ flags.make_flags('remote-path', remote_path)
|
+ flags.make_flags('remote-path', remote_path)
|
||||||
+ flags.make_flags('umask', config.get('umask'))
|
+ flags.make_flags('umask', config.get('umask'))
|
||||||
|
+ flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ flags.make_flags('lock-wait', lock_wait)
|
+ flags.make_flags('lock-wait', lock_wait)
|
||||||
+ ('--log-json',)
|
|
||||||
+ (('--json',) if repo_info_arguments.json else ())
|
+ (('--json',) if repo_info_arguments.json else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,14 +54,12 @@ def display_repository_info(
|
|||||||
borg_exit_codes = config.get('borg_exit_codes')
|
borg_exit_codes = config.get('borg_exit_codes')
|
||||||
|
|
||||||
if repo_info_arguments.json:
|
if repo_info_arguments.json:
|
||||||
return '\n'.join(
|
return execute_command_and_capture_output(
|
||||||
execute_command_and_capture_output(
|
full_command,
|
||||||
full_command,
|
environment=environment.make_environment(config),
|
||||||
environment=environment.make_environment(config),
|
working_directory=working_directory,
|
||||||
working_directory=working_directory,
|
borg_local_path=local_path,
|
||||||
borg_local_path=local_path,
|
borg_exit_codes=borg_exit_codes,
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
execute_command(
|
execute_command(
|
||||||
@@ -83,33 +72,3 @@ def display_repository_info(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return 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 configuration dict, the local Borg version, global
|
|
||||||
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
|
|
||||||
|
|
||||||
Raise ValueError if the Borg repository ID cannot be determined.
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
return json.loads(
|
|
||||||
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}')
|
|
||||||
|
|||||||
+29
-55
@@ -1,12 +1,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
from borgmatic.borg import environment, feature, flags
|
from borgmatic.borg import environment, feature, flags
|
||||||
from borgmatic.execute import execute_command_and_capture_output
|
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -63,10 +62,6 @@ def get_latest_archive(
|
|||||||
|
|
||||||
Raises ValueError if there are no archives in the repository.
|
Raises ValueError if there are no archives in the repository.
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get(
|
|
||||||
'repo_list' if feature.available(feature.Feature.REPO_LIST, local_borg_version) else 'list',
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
local_path,
|
local_path,
|
||||||
@@ -77,15 +72,8 @@ def get_latest_archive(
|
|||||||
),
|
),
|
||||||
*flags.make_flags('remote-path', remote_path),
|
*flags.make_flags('remote-path', remote_path),
|
||||||
*flags.make_flags('umask', config.get('umask')),
|
*flags.make_flags('umask', config.get('umask')),
|
||||||
*('--log-json',),
|
*flags.make_flags('log-json', config.get('log_json')),
|
||||||
*flags.make_flags('lock-wait', config.get('lock_wait')),
|
*flags.make_flags('lock-wait', config.get('lock_wait')),
|
||||||
*(
|
|
||||||
flags.make_match_archives_flags(
|
|
||||||
config.get('match_archives'),
|
|
||||||
config.get('archive_name_format'),
|
|
||||||
local_borg_version,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
*(
|
*(
|
||||||
flags.make_flags('consider-checkpoints', consider_checkpoints)
|
flags.make_flags('consider-checkpoints', consider_checkpoints)
|
||||||
if not feature.available(feature.Feature.REPO_LIST, local_borg_version)
|
if not feature.available(feature.Feature.REPO_LIST, local_borg_version)
|
||||||
@@ -93,18 +81,15 @@ def get_latest_archive(
|
|||||||
),
|
),
|
||||||
*flags.make_flags('last', 1),
|
*flags.make_flags('last', 1),
|
||||||
'--json',
|
'--json',
|
||||||
*(tuple(shlex.split(extra_borg_options)) if extra_borg_options else ()),
|
|
||||||
*flags.make_repository_flags(repository_path, local_borg_version),
|
*flags.make_repository_flags(repository_path, local_borg_version),
|
||||||
)
|
)
|
||||||
|
|
||||||
json_output = '\n'.join(
|
json_output = execute_command_and_capture_output(
|
||||||
execute_command_and_capture_output(
|
full_command,
|
||||||
full_command,
|
environment=environment.make_environment(config),
|
||||||
environment=environment.make_environment(config),
|
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
borg_local_path=local_path,
|
||||||
borg_local_path=local_path,
|
borg_exit_codes=config.get('borg_exit_codes'),
|
||||||
borg_exit_codes=config.get('borg_exit_codes'),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
archives = json.loads(json_output)['archives']
|
archives = json.loads(json_output)['archives']
|
||||||
@@ -119,7 +104,7 @@ def get_latest_archive(
|
|||||||
return latest_archive
|
return latest_archive
|
||||||
|
|
||||||
|
|
||||||
MAKE_FLAGS_EXCLUDES = ('repository', 'format', 'prefix', 'match_archives')
|
MAKE_FLAGS_EXCLUDES = ('repository', 'prefix', 'match_archives')
|
||||||
|
|
||||||
|
|
||||||
def make_repo_list_command(
|
def make_repo_list_command(
|
||||||
@@ -136,11 +121,6 @@ def make_repo_list_command(
|
|||||||
arguments to the repo_list action, global arguments as an argparse.Namespace instance, and local and
|
arguments to the repo_list action, global arguments as an argparse.Namespace instance, and local and
|
||||||
remote Borg paths, return a command as a tuple to list archives with a repository.
|
remote Borg paths, return a command as a tuple to list archives with a repository.
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get(
|
|
||||||
'repo_list' if feature.available(feature.Feature.REPO_LIST, local_borg_version) else 'list',
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
local_path,
|
local_path,
|
||||||
@@ -162,7 +142,7 @@ def make_repo_list_command(
|
|||||||
)
|
)
|
||||||
+ flags.make_flags('remote-path', remote_path)
|
+ flags.make_flags('remote-path', remote_path)
|
||||||
+ flags.make_flags('umask', config.get('umask'))
|
+ flags.make_flags('umask', config.get('umask'))
|
||||||
+ ('--log-json',)
|
+ flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||||
+ (
|
+ (
|
||||||
(
|
(
|
||||||
@@ -179,11 +159,7 @@ def make_repo_list_command(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
+ flags.make_flags(
|
|
||||||
'format', repo_list_arguments.format or config.get('archive_list_format')
|
|
||||||
)
|
|
||||||
+ flags.make_flags_from_arguments(repo_list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
|
+ flags.make_flags_from_arguments(repo_list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,28 +202,26 @@ def list_repository(
|
|||||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||||
borg_exit_codes = config.get('borg_exit_codes')
|
borg_exit_codes = config.get('borg_exit_codes')
|
||||||
|
|
||||||
if repo_list_arguments.json:
|
json_listing = execute_command_and_capture_output(
|
||||||
return '\n'.join(
|
json_command,
|
||||||
execute_command_and_capture_output(
|
environment=environment.make_environment(config),
|
||||||
json_command,
|
working_directory=working_directory,
|
||||||
environment=environment.make_environment(config),
|
borg_local_path=local_path,
|
||||||
working_directory=working_directory,
|
borg_exit_codes=borg_exit_codes,
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
output_lines = tuple(
|
|
||||||
execute_command_and_capture_output(
|
|
||||||
main_command,
|
|
||||||
output_log_level=logging.ANSWER,
|
|
||||||
environment=environment.make_environment(config),
|
|
||||||
working_directory=working_directory,
|
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
flags.warn_for_aggressive_archive_flags(main_command, output_lines)
|
if repo_list_arguments.json:
|
||||||
|
return json_listing
|
||||||
|
|
||||||
|
flags.warn_for_aggressive_archive_flags(json_command, json_listing)
|
||||||
|
|
||||||
|
execute_command(
|
||||||
|
main_command,
|
||||||
|
output_log_level=logging.ANSWER,
|
||||||
|
environment=environment.make_environment(config),
|
||||||
|
working_directory=working_directory,
|
||||||
|
borg_local_path=local_path,
|
||||||
|
borg_exit_codes=borg_exit_codes,
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
DEFAULT_BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic' # pragma: nocover
|
DEFAULT_BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
@@ -25,7 +24,6 @@ def transfer_archives(
|
|||||||
instance, transfer archives to the given repository.
|
instance, transfer archives to the given repository.
|
||||||
'''
|
'''
|
||||||
borgmatic.logger.add_custom_log_levels()
|
borgmatic.logger.add_custom_log_levels()
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('transfer', '')
|
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'transfer')
|
(local_path, 'transfer')
|
||||||
@@ -33,7 +31,7 @@ def transfer_archives(
|
|||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ flags.make_flags('remote-path', remote_path)
|
+ flags.make_flags('remote-path', remote_path)
|
||||||
+ flags.make_flags('umask', config.get('umask'))
|
+ flags.make_flags('umask', config.get('umask'))
|
||||||
+ (('--log-json',) if (config.get('log_json') or not config.get('progress')) else ())
|
+ flags.make_flags('log-json', config.get('log_json'))
|
||||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||||
+ flags.make_flags('progress', config.get('progress'))
|
+ flags.make_flags('progress', config.get('progress'))
|
||||||
+ (
|
+ (
|
||||||
@@ -55,7 +53,6 @@ def transfer_archives(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||||
+ flags.make_flags('other-repo', transfer_arguments.source_repository)
|
+ flags.make_flags('other-repo', transfer_arguments.source_repository)
|
||||||
+ flags.make_flags('dry-run', dry_run)
|
+ flags.make_flags('dry-run', dry_run)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import shlex
|
|
||||||
|
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command
|
||||||
@@ -12,13 +11,10 @@ def unmount_archive(config, mount_point, local_path='borg'):
|
|||||||
Given a mounted filesystem mount point, and an optional local Borg paths, umount the filesystem
|
Given a mounted filesystem mount point, and an optional local Borg paths, umount the filesystem
|
||||||
from the mount point.
|
from the mount point.
|
||||||
'''
|
'''
|
||||||
extra_borg_options = config.get('extra_borg_options', {}).get('umount', '')
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'umount')
|
(local_path, 'umount')
|
||||||
+ ('--log-json',)
|
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
+ (tuple(shlex.split(extra_borg_options)) if extra_borg_options else ())
|
|
||||||
+ (mount_point,)
|
+ (mount_point,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,19 +16,15 @@ def local_borg_version(config, local_path='borg'):
|
|||||||
'''
|
'''
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, '--version')
|
(local_path, '--version')
|
||||||
+ ('--log-json',)
|
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
)
|
)
|
||||||
|
output = execute_command_and_capture_output(
|
||||||
output = '\n'.join(
|
full_command,
|
||||||
execute_command_and_capture_output(
|
environment=environment.make_environment(config),
|
||||||
full_command,
|
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||||
environment=environment.make_environment(config),
|
borg_local_path=local_path,
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
borg_exit_codes=config.get('borg_exit_codes'),
|
||||||
borg_local_path=local_path,
|
|
||||||
borg_exit_codes=config.get('borg_exit_codes'),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ ACTION_ALIASES = {
|
|||||||
'key': [],
|
'key': [],
|
||||||
'borg': [],
|
'borg': [],
|
||||||
'recreate': [],
|
'recreate': [],
|
||||||
'diff': [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -298,14 +297,7 @@ def parse_arguments_for_actions(unparsed_arguments, action_parsers, global_parse
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
OMITTED_FLAG_NAMES = {
|
OMITTED_FLAG_NAMES = {'match-archives', 'progress', 'statistics', 'list-details'}
|
||||||
'match-archives',
|
|
||||||
'progress',
|
|
||||||
'statistics',
|
|
||||||
'list-details',
|
|
||||||
'file-list-format',
|
|
||||||
'archive-list-format',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def make_argument_description(schema, flag_name):
|
def make_argument_description(schema, flag_name):
|
||||||
@@ -576,7 +568,8 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
ignoring actions, and the combined parser is handy for displaying help that includes everything:
|
ignoring actions, and the combined parser is handy for displaying help that includes everything:
|
||||||
global flags, a list of actions, etc.
|
global flags, a list of actions, etc.
|
||||||
'''
|
'''
|
||||||
config_paths = collect.get_default_config_paths()
|
config_paths = collect.get_default_config_paths(expand_home=True)
|
||||||
|
unexpanded_config_paths = collect.get_default_config_paths(expand_home=False)
|
||||||
|
|
||||||
# Using allow_abbrev=False here prevents the global parser from erroring about "ambiguous"
|
# Using allow_abbrev=False here prevents the global parser from erroring about "ambiguous"
|
||||||
# options like --encryption. Such options are intended for an action parser rather than the
|
# options like --encryption. Such options are intended for an action parser rather than the
|
||||||
@@ -589,7 +582,7 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
'--config',
|
'--config',
|
||||||
dest='config_paths',
|
dest='config_paths',
|
||||||
action='append',
|
action='append',
|
||||||
help='Configuration filename or directory, can specify flag multiple times, defaults to /etc/borgmatic/config.yaml, /etc/borgmatic.d, $XDG_CONFIG_HOME/borgmatic/config.yaml, and $XDG_CONFIG_HOME/borgmatic.d, where $XDG_CONFIG_HOME defaults to $HOME/.config',
|
help=f"Configuration filename or directory, can specify flag multiple times, defaults to: -c {' -c '.join(unexpanded_config_paths)}",
|
||||||
)
|
)
|
||||||
global_group.add_argument(
|
global_group.add_argument(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -648,8 +641,8 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
repo_create_parser = action_parsers.add_parser(
|
repo_create_parser = action_parsers.add_parser(
|
||||||
'repo-create',
|
'repo-create',
|
||||||
aliases=ACTION_ALIASES['repo-create'],
|
aliases=ACTION_ALIASES['repo-create'],
|
||||||
help='Create a new, empty Borg repository (also known as "init")',
|
help='Create a new, empty Borg repository',
|
||||||
description='Create a new, empty Borg repository (also known as "init")',
|
description='Create a new, empty Borg repository',
|
||||||
add_help=False,
|
add_help=False,
|
||||||
)
|
)
|
||||||
repo_create_group = repo_create_parser.add_argument_group('repo-create arguments')
|
repo_create_group = repo_create_parser.add_argument_group('repo-create arguments')
|
||||||
@@ -1220,9 +1213,9 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
config_generate_group.add_argument(
|
config_generate_group.add_argument(
|
||||||
'-d',
|
'-d',
|
||||||
'--destination',
|
'--destination',
|
||||||
dest='destination_path',
|
dest='destination_filename',
|
||||||
default=config_paths[0],
|
default=config_paths[0],
|
||||||
help='Destination configuration file (or directory if using --split), default: /etc/borgmatic/config.yaml',
|
help=f'Destination configuration file, default: {unexpanded_config_paths[0]}',
|
||||||
)
|
)
|
||||||
config_generate_group.add_argument(
|
config_generate_group.add_argument(
|
||||||
'--overwrite',
|
'--overwrite',
|
||||||
@@ -1230,11 +1223,6 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Whether to overwrite any existing destination file, defaults to false',
|
help='Whether to overwrite any existing destination file, defaults to false',
|
||||||
)
|
)
|
||||||
config_generate_group.add_argument(
|
|
||||||
'--split',
|
|
||||||
action='store_true',
|
|
||||||
help='Assuming the destination is a directory instead of a file, split the configuration into separate files within it, one per option, useful for documentation',
|
|
||||||
)
|
|
||||||
config_generate_group.add_argument(
|
config_generate_group.add_argument(
|
||||||
'-h',
|
'-h',
|
||||||
'--help',
|
'--help',
|
||||||
@@ -1262,29 +1250,6 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
help='Show this help message and exit',
|
help='Show this help message and exit',
|
||||||
)
|
)
|
||||||
|
|
||||||
config_show_parser = config_parsers.add_parser(
|
|
||||||
'show',
|
|
||||||
help='Show the computed configuration for each file specified with --config (see borgmatic --help)',
|
|
||||||
description='Show the computed configuration for each file specified with --config (see borgmatic --help)',
|
|
||||||
add_help=False,
|
|
||||||
)
|
|
||||||
config_show_group = config_show_parser.add_argument_group('config show arguments')
|
|
||||||
config_show_group.add_argument(
|
|
||||||
'--option',
|
|
||||||
help='Show the value of a single named configuration option instead of the entire configuration',
|
|
||||||
)
|
|
||||||
config_show_group.add_argument(
|
|
||||||
'--json',
|
|
||||||
action='store_true',
|
|
||||||
help='Show the configuration as JSON with one array element per configuration file',
|
|
||||||
)
|
|
||||||
config_show_group.add_argument(
|
|
||||||
'-h',
|
|
||||||
'--help',
|
|
||||||
action='help',
|
|
||||||
help='Show this help message and exit',
|
|
||||||
)
|
|
||||||
|
|
||||||
export_tar_parser = action_parsers.add_parser(
|
export_tar_parser = action_parsers.add_parser(
|
||||||
'export-tar',
|
'export-tar',
|
||||||
aliases=ACTION_ALIASES['export-tar'],
|
aliases=ACTION_ALIASES['export-tar'],
|
||||||
@@ -1572,7 +1537,7 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Output only archive names',
|
help='Output only archive names',
|
||||||
)
|
)
|
||||||
repo_list_group.add_argument('--format', help='Borg format for the archive listing')
|
repo_list_group.add_argument('--format', help='Format for archive listing')
|
||||||
repo_list_group.add_argument(
|
repo_list_group.add_argument(
|
||||||
'--json',
|
'--json',
|
||||||
default=False,
|
default=False,
|
||||||
@@ -1675,7 +1640,7 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Output only path names',
|
help='Output only path names',
|
||||||
)
|
)
|
||||||
list_group.add_argument('--format', help='Borg format for the file listing')
|
list_group.add_argument('--format', help='Format for file listing')
|
||||||
list_group.add_argument(
|
list_group.add_argument(
|
||||||
'--json',
|
'--json',
|
||||||
default=False,
|
default=False,
|
||||||
@@ -1955,7 +1920,7 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
)
|
)
|
||||||
recreate_group.add_argument(
|
recreate_group.add_argument(
|
||||||
'--archive',
|
'--archive',
|
||||||
help='Archive name, hash, or series to recreate, defaults to all archives in the repository (if specified), or all archives across all repositories',
|
help='Archive name, hash, or series to recreate',
|
||||||
)
|
)
|
||||||
recreate_group.add_argument(
|
recreate_group.add_argument(
|
||||||
'--list',
|
'--list',
|
||||||
@@ -1994,47 +1959,6 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
|||||||
help='Show this help message and exit',
|
help='Show this help message and exit',
|
||||||
)
|
)
|
||||||
|
|
||||||
diff_parser = action_parsers.add_parser(
|
|
||||||
'diff',
|
|
||||||
aliases=ACTION_ALIASES['diff'],
|
|
||||||
help='This command finds differences (file contents, user/group/mode) between archives',
|
|
||||||
description='This command finds differences (file contents, user/group/mode) between archives',
|
|
||||||
)
|
|
||||||
diff_group = diff_parser.add_argument_group('diff arguments')
|
|
||||||
diff_group.add_argument(
|
|
||||||
'--repository',
|
|
||||||
help='Path of repository containing archive to diff, defaults to the configured repository if there is only one, quoted globs supported',
|
|
||||||
)
|
|
||||||
diff_group.add_argument(
|
|
||||||
'--archive',
|
|
||||||
help='Archive name, hash, or series to diff',
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
diff_group.add_argument(
|
|
||||||
'--second-archive',
|
|
||||||
help='Second archive name, hash, or series to diff',
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
diff_group.add_argument(
|
|
||||||
'--same-chunker-params', action='store_true', help='Override check of chunker parameters'
|
|
||||||
)
|
|
||||||
diff_group.add_argument(
|
|
||||||
'--sort-by',
|
|
||||||
metavar='KEY',
|
|
||||||
dest='sort_keys',
|
|
||||||
action='append',
|
|
||||||
help='Advanced sorting: specify field(s) to sort by. Prefix with > for descending or < for ascending (default)',
|
|
||||||
)
|
|
||||||
diff_group.add_argument(
|
|
||||||
'--content-only',
|
|
||||||
action='store_true',
|
|
||||||
help='Only compare differences in content (exclude metadata differences)',
|
|
||||||
)
|
|
||||||
diff_group.add_argument(
|
|
||||||
'--only-patterns',
|
|
||||||
action='store_true',
|
|
||||||
help='Run the diff according to borgmatic configured patterns (ie do not diff entire archives)',
|
|
||||||
)
|
|
||||||
borg_parser = action_parsers.add_parser(
|
borg_parser = action_parsers.add_parser(
|
||||||
'borg',
|
'borg',
|
||||||
aliases=ACTION_ALIASES['borg'],
|
aliases=ACTION_ALIASES['borg'],
|
||||||
@@ -2086,7 +2010,7 @@ def parse_arguments(schema, *unparsed_arguments):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not arguments['global'].config_paths:
|
if not arguments['global'].config_paths:
|
||||||
arguments['global'].config_paths = collect.get_default_config_paths()
|
arguments['global'].config_paths = collect.get_default_config_paths(expand_home=True)
|
||||||
|
|
||||||
for action_name in ('bootstrap', 'generate', 'validate'):
|
for action_name in ('bootstrap', 'generate', 'validate'):
|
||||||
if action_name in arguments and len(arguments) > HIGHLANDER_ACTION_ARGUMENTS_COUNT:
|
if action_name in arguments and len(arguments) > HIGHLANDER_ACTION_ARGUMENTS_COUNT:
|
||||||
|
|||||||
+72
-116
@@ -17,11 +17,9 @@ import borgmatic.actions.check
|
|||||||
import borgmatic.actions.compact
|
import borgmatic.actions.compact
|
||||||
import borgmatic.actions.config.bootstrap
|
import borgmatic.actions.config.bootstrap
|
||||||
import borgmatic.actions.config.generate
|
import borgmatic.actions.config.generate
|
||||||
import borgmatic.actions.config.show
|
|
||||||
import borgmatic.actions.config.validate
|
import borgmatic.actions.config.validate
|
||||||
import borgmatic.actions.create
|
import borgmatic.actions.create
|
||||||
import borgmatic.actions.delete
|
import borgmatic.actions.delete
|
||||||
import borgmatic.actions.diff
|
|
||||||
import borgmatic.actions.export_key
|
import borgmatic.actions.export_key
|
||||||
import borgmatic.actions.export_tar
|
import borgmatic.actions.export_tar
|
||||||
import borgmatic.actions.extract
|
import borgmatic.actions.extract
|
||||||
@@ -342,7 +340,7 @@ def run_actions( # noqa: PLR0912, PLR0915
|
|||||||
'''
|
'''
|
||||||
Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration
|
Given parsed command-line arguments as an argparse.ArgumentParser instance, the configuration
|
||||||
filename, a configuration dict, a sequence of loaded configuration paths, local and remote paths
|
filename, a configuration dict, a sequence of loaded configuration paths, local and remote paths
|
||||||
to Borg, a local Borg version string, and a repository dict, run all actions from the
|
to Borg, a local Borg version string, and a repository name, run all actions from the
|
||||||
command-line arguments on the given repository.
|
command-line arguments on the given repository.
|
||||||
|
|
||||||
Yield JSON output strings from executing any actions that produce JSON.
|
Yield JSON output strings from executing any actions that produce JSON.
|
||||||
@@ -364,22 +362,6 @@ def run_actions( # noqa: PLR0912, PLR0915
|
|||||||
'repository': repository_path,
|
'repository': repository_path,
|
||||||
}
|
}
|
||||||
skip_actions = set(get_skip_actions(config, arguments))
|
skip_actions = set(get_skip_actions(config, arguments))
|
||||||
requested_repository = next(
|
|
||||||
(
|
|
||||||
repository
|
|
||||||
for action_arguments in arguments.values()
|
|
||||||
for repository in (getattr(action_arguments, 'repository', None),)
|
|
||||||
if repository is not None
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if requested_repository and not borgmatic.config.validate.repositories_match(
|
|
||||||
repository,
|
|
||||||
requested_repository,
|
|
||||||
):
|
|
||||||
logger.debug('Skipping actions because the requested --repository does not match')
|
|
||||||
return
|
|
||||||
|
|
||||||
with borgmatic.hooks.command.Before_after_hooks(
|
with borgmatic.hooks.command.Before_after_hooks(
|
||||||
command_hooks=config.get('commands'),
|
command_hooks=config.get('commands'),
|
||||||
@@ -443,7 +425,6 @@ def run_actions( # noqa: PLR0912, PLR0915
|
|||||||
local_borg_version,
|
local_borg_version,
|
||||||
action_arguments,
|
action_arguments,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
dry_run_label,
|
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
@@ -482,6 +463,7 @@ def run_actions( # noqa: PLR0912, PLR0915
|
|||||||
global_arguments,
|
global_arguments,
|
||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
|
hook_context,
|
||||||
)
|
)
|
||||||
elif action_name == 'extract':
|
elif action_name == 'extract':
|
||||||
borgmatic.actions.extract.run_extract(
|
borgmatic.actions.extract.run_extract(
|
||||||
@@ -624,16 +606,6 @@ def run_actions( # noqa: PLR0912, PLR0915
|
|||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
elif action_name == 'diff':
|
|
||||||
borgmatic.actions.diff.run_diff(
|
|
||||||
repository,
|
|
||||||
config,
|
|
||||||
local_borg_version,
|
|
||||||
action_arguments,
|
|
||||||
global_arguments,
|
|
||||||
local_path,
|
|
||||||
remote_path,
|
|
||||||
)
|
|
||||||
elif action_name == 'borg':
|
elif action_name == 'borg':
|
||||||
borgmatic.actions.borg.run_borg(
|
borgmatic.actions.borg.run_borg(
|
||||||
repository,
|
repository,
|
||||||
@@ -673,7 +645,6 @@ def load_configurations(config_filenames, arguments, overrides=None, resolve_env
|
|||||||
levelno=logging.DEBUG,
|
levelno=logging.DEBUG,
|
||||||
levelname='DEBUG',
|
levelname='DEBUG',
|
||||||
msg=f'{config_filename}: Loading configuration file',
|
msg=f'{config_filename}: Loading configuration file',
|
||||||
name=logger.name,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -696,7 +667,6 @@ def load_configurations(config_filenames, arguments, overrides=None, resolve_env
|
|||||||
levelno=logging.CRITICAL,
|
levelno=logging.CRITICAL,
|
||||||
levelname='CRITICAL',
|
levelname='CRITICAL',
|
||||||
msg=f'{config_filename}: Insufficient permissions to read configuration file',
|
msg=f'{config_filename}: Insufficient permissions to read configuration file',
|
||||||
name=logger.name,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -709,16 +679,10 @@ def load_configurations(config_filenames, arguments, overrides=None, resolve_env
|
|||||||
levelno=logging.CRITICAL,
|
levelno=logging.CRITICAL,
|
||||||
levelname='CRITICAL',
|
levelname='CRITICAL',
|
||||||
msg=f'{config_filename}: Error parsing configuration file',
|
msg=f'{config_filename}: Error parsing configuration file',
|
||||||
name=logger.name,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
logging.makeLogRecord(
|
logging.makeLogRecord(
|
||||||
dict(
|
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=str(error)),
|
||||||
levelno=logging.CRITICAL,
|
|
||||||
levelname='CRITICAL',
|
|
||||||
msg=str(error),
|
|
||||||
name=logger.name,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -731,7 +695,7 @@ def log_record(suppress_log=False, **kwargs):
|
|||||||
Create a log record based on the given makeLogRecord() arguments, one of which must be
|
Create a log record based on the given makeLogRecord() arguments, one of which must be
|
||||||
named "levelno". Log the record (unless suppress log is set) and return it.
|
named "levelno". Log the record (unless suppress log is set) and return it.
|
||||||
'''
|
'''
|
||||||
record = logging.makeLogRecord(dict(kwargs, name=logger.name))
|
record = logging.makeLogRecord(kwargs)
|
||||||
if suppress_log:
|
if suppress_log:
|
||||||
return record
|
return record
|
||||||
|
|
||||||
@@ -822,18 +786,18 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
|||||||
'''
|
'''
|
||||||
add_custom_log_levels()
|
add_custom_log_levels()
|
||||||
|
|
||||||
try:
|
if 'bootstrap' in arguments:
|
||||||
if 'bootstrap' in arguments:
|
try:
|
||||||
try:
|
# No configuration file is needed for bootstrap.
|
||||||
# No configuration file is needed for bootstrap.
|
local_borg_version = borg_version.local_borg_version(
|
||||||
local_borg_version = borg_version.local_borg_version(
|
{},
|
||||||
{},
|
arguments['bootstrap'].local_path,
|
||||||
arguments['bootstrap'].local_path,
|
)
|
||||||
)
|
except (OSError, CalledProcessError, ValueError) as error:
|
||||||
except (OSError, CalledProcessError, ValueError) as error:
|
yield from log_error_records('Error getting local Borg version', error)
|
||||||
yield from log_error_records('Error getting local Borg version', error)
|
return
|
||||||
return
|
|
||||||
|
|
||||||
|
try:
|
||||||
borgmatic.actions.config.bootstrap.run_bootstrap(
|
borgmatic.actions.config.bootstrap.run_bootstrap(
|
||||||
arguments['bootstrap'],
|
arguments['bootstrap'],
|
||||||
arguments['global'],
|
arguments['global'],
|
||||||
@@ -844,13 +808,19 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
|||||||
levelno=logging.ANSWER,
|
levelno=logging.ANSWER,
|
||||||
levelname='ANSWER',
|
levelname='ANSWER',
|
||||||
msg='Bootstrap successful',
|
msg='Bootstrap successful',
|
||||||
name=logger.name,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
except (
|
||||||
|
CalledProcessError,
|
||||||
|
ValueError,
|
||||||
|
OSError,
|
||||||
|
) as error:
|
||||||
|
yield from log_error_records(error)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'generate' in arguments:
|
if 'generate' in arguments:
|
||||||
|
try:
|
||||||
borgmatic.actions.config.generate.run_generate(
|
borgmatic.actions.config.generate.run_generate(
|
||||||
arguments['generate'],
|
arguments['generate'],
|
||||||
arguments['global'],
|
arguments['global'],
|
||||||
@@ -860,25 +830,30 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
|||||||
levelno=logging.ANSWER,
|
levelno=logging.ANSWER,
|
||||||
levelname='ANSWER',
|
levelname='ANSWER',
|
||||||
msg='Generate successful',
|
msg='Generate successful',
|
||||||
name=logger.name,
|
),
|
||||||
|
)
|
||||||
|
except (
|
||||||
|
CalledProcessError,
|
||||||
|
ValueError,
|
||||||
|
OSError,
|
||||||
|
) as error:
|
||||||
|
yield from log_error_records(error)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'validate' in arguments:
|
||||||
|
if configuration_parse_errors:
|
||||||
|
yield logging.makeLogRecord(
|
||||||
|
dict(
|
||||||
|
levelno=logging.CRITICAL,
|
||||||
|
levelname='CRITICAL',
|
||||||
|
msg='Configuration validation failed',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'validate' in arguments:
|
try:
|
||||||
if configuration_parse_errors:
|
|
||||||
yield logging.makeLogRecord(
|
|
||||||
dict(
|
|
||||||
levelno=logging.CRITICAL,
|
|
||||||
levelname='CRITICAL',
|
|
||||||
msg='Configuration validation failed',
|
|
||||||
name=logger.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
borgmatic.actions.config.validate.run_validate(arguments['validate'], configs)
|
borgmatic.actions.config.validate.run_validate(arguments['validate'], configs)
|
||||||
|
|
||||||
yield logging.makeLogRecord(
|
yield logging.makeLogRecord(
|
||||||
@@ -886,23 +861,16 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
|||||||
levelno=logging.ANSWER,
|
levelno=logging.ANSWER,
|
||||||
levelname='ANSWER',
|
levelname='ANSWER',
|
||||||
msg='All configuration files are valid',
|
msg='All configuration files are valid',
|
||||||
name=logger.name,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
except (
|
||||||
|
CalledProcessError,
|
||||||
|
ValueError,
|
||||||
|
OSError,
|
||||||
|
) as error:
|
||||||
|
yield from log_error_records(error)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'show' in arguments:
|
|
||||||
borgmatic.actions.config.show.run_show(arguments['show'], configs)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
except (
|
|
||||||
CalledProcessError,
|
|
||||||
ValueError,
|
|
||||||
OSError,
|
|
||||||
) as error:
|
|
||||||
yield from log_error_records(error)
|
|
||||||
|
|
||||||
|
|
||||||
def collect_configuration_run_summary_logs(configs, config_paths, arguments, log_file_path): # noqa: PLR0912
|
def collect_configuration_run_summary_logs(configs, config_paths, arguments, log_file_path): # noqa: PLR0912
|
||||||
@@ -985,7 +953,6 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments, log
|
|||||||
levelno=logging.INFO,
|
levelno=logging.INFO,
|
||||||
levelname='INFO',
|
levelname='INFO',
|
||||||
msg=f'{config_filename}: Successfully ran configuration file',
|
msg=f'{config_filename}: Successfully ran configuration file',
|
||||||
name=logger.name,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if results:
|
if results:
|
||||||
@@ -1044,17 +1011,16 @@ def exit_with_help_link(): # pragma: no cover
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def check_and_show_help_on_no_args(configs, schema):
|
def check_and_show_help_on_no_args(configs):
|
||||||
'''
|
'''
|
||||||
Given a dict of configuration filename to corresponding parsed configuration and the
|
Given a dict of configuration filename to corresponding parsed configuration, check if the
|
||||||
configuration schema as a dict, check if the borgmatic command was run without any arguments. If
|
borgmatic command is run without any arguments. If the configuration option "default_actions" is
|
||||||
the configuration option "default_actions" is set to False, then show the help message an exit.
|
set to False, show the help message. Otherwise, trigger the default backup behavior.
|
||||||
'''
|
'''
|
||||||
if len(sys.argv) == 1: # No arguments provided.
|
if len(sys.argv) == 1: # No arguments provided
|
||||||
default_actions = any(config.get('default_actions', True) for config in configs.values())
|
default_actions = any(config.get('default_actions', True) for config in configs.values())
|
||||||
|
if not default_actions:
|
||||||
if configs and not default_actions:
|
parse_arguments('--help')
|
||||||
parse_arguments(schema, '--help')
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
@@ -1086,23 +1052,6 @@ def get_singular_option_value(configs, option_name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def display_summary(summary_logs, log_json): # pragma: no cover
|
|
||||||
summary_logs_max_level = max(log.levelno for log in summary_logs)
|
|
||||||
|
|
||||||
for message in ('summary:',) if log_json else ('', 'summary:'):
|
|
||||||
log_record(
|
|
||||||
levelno=summary_logs_max_level,
|
|
||||||
levelname=logging.getLevelName(summary_logs_max_level),
|
|
||||||
msg=message,
|
|
||||||
)
|
|
||||||
|
|
||||||
for log in summary_logs:
|
|
||||||
logger.handle(log)
|
|
||||||
|
|
||||||
if summary_logs_max_level >= logging.CRITICAL:
|
|
||||||
exit_with_help_link()
|
|
||||||
|
|
||||||
|
|
||||||
def main(extra_summary_logs=()): # pragma: no cover
|
def main(extra_summary_logs=()): # pragma: no cover
|
||||||
configure_signals()
|
configure_signals()
|
||||||
configure_delayed_logging()
|
configure_delayed_logging()
|
||||||
@@ -1123,7 +1072,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
|||||||
exit_with_help_link()
|
exit_with_help_link()
|
||||||
except SystemExit as error:
|
except SystemExit as error:
|
||||||
if error.code == 0:
|
if error.code == 0:
|
||||||
raise
|
raise error
|
||||||
|
|
||||||
configure_logging(logging.CRITICAL)
|
configure_logging(logging.CRITICAL)
|
||||||
logger.critical(f"Error parsing arguments: {' '.join(sys.argv)}")
|
logger.critical(f"Error parsing arguments: {' '.join(sys.argv)}")
|
||||||
@@ -1151,7 +1100,8 @@ def main(extra_summary_logs=()): # pragma: no cover
|
|||||||
resolve_env=global_arguments.resolve_env and not arguments.get('validate'),
|
resolve_env=global_arguments.resolve_env and not arguments.get('validate'),
|
||||||
)
|
)
|
||||||
|
|
||||||
check_and_show_help_on_no_args(configs, schema)
|
# Use the helper function to check and show help on no arguments, passing the preloaded configs
|
||||||
|
check_and_show_help_on_no_args(configs)
|
||||||
|
|
||||||
configuration_parse_errors = (
|
configuration_parse_errors = (
|
||||||
(max(log.levelno for log in parse_logs) >= logging.CRITICAL) if parse_logs else False
|
(max(log.levelno for log in parse_logs) >= logging.CRITICAL) if parse_logs else False
|
||||||
@@ -1161,7 +1111,6 @@ def main(extra_summary_logs=()): # pragma: no cover
|
|||||||
getattr(sub_arguments, 'json', False) for sub_arguments in arguments.values()
|
getattr(sub_arguments, 'json', False) for sub_arguments in arguments.values()
|
||||||
)
|
)
|
||||||
log_file_path = get_singular_option_value(configs, 'log_file')
|
log_file_path = get_singular_option_value(configs, 'log_file')
|
||||||
log_json = get_singular_option_value(configs, 'log_json')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
configure_logging(
|
configure_logging(
|
||||||
@@ -1171,8 +1120,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
|||||||
verbosity_to_log_level(get_verbosity(configs, 'monitoring_verbosity')),
|
verbosity_to_log_level(get_verbosity(configs, 'monitoring_verbosity')),
|
||||||
log_file_path,
|
log_file_path,
|
||||||
get_singular_option_value(configs, 'log_file_format'),
|
get_singular_option_value(configs, 'log_file_format'),
|
||||||
log_json,
|
color_enabled=should_do_markup(configs, any_json_flags),
|
||||||
color_enabled=should_do_markup(configs, any_json_flags or log_json),
|
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, PermissionError) as error:
|
except (FileNotFoundError, PermissionError) as error:
|
||||||
configure_logging(logging.CRITICAL)
|
configure_logging(logging.CRITICAL)
|
||||||
@@ -1200,9 +1148,17 @@ def main(extra_summary_logs=()): # pragma: no cover
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
summary_logs_max_level = max(log.levelno for log in summary_logs)
|
||||||
|
|
||||||
display_summary(summary_logs, log_json)
|
for message in ('', 'summary:'):
|
||||||
|
log_record(
|
||||||
|
levelno=summary_logs_max_level,
|
||||||
|
levelname=logging.getLevelName(summary_logs_max_level),
|
||||||
|
msg=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
for log in summary_logs:
|
||||||
|
logger.handle(log)
|
||||||
|
|
||||||
if __name__ == '__main__': # pragma: no cover
|
if summary_logs_max_level >= logging.CRITICAL:
|
||||||
main()
|
exit_with_help_link()
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def bash_completion():
|
|||||||
'check_version() {',
|
'check_version() {',
|
||||||
' local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
|
' local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
|
||||||
' local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
|
' local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
|
||||||
' if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];',
|
' if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
|
||||||
f''' then cat << EOF\n{borgmatic.commands.completion.actions.upgrade_message(
|
f''' then cat << EOF\n{borgmatic.commands.completion.actions.upgrade_message(
|
||||||
'bash',
|
'bash',
|
||||||
'sudo sh -c "borgmatic --bash-completion > $BASH_SOURCE"',
|
'sudo sh -c "borgmatic --bash-completion > $BASH_SOURCE"',
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def get_default_config_paths():
|
def get_default_config_paths(expand_home=True):
|
||||||
'''
|
'''
|
||||||
Return a list of default configuration paths. This includes both system-wide
|
Based on the value of the XDG_CONFIG_HOME and HOME environment variables, return a list of
|
||||||
configuration and configuration in the current user's home directory.
|
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.path.expandvars(
|
user_config_directory = os.getenv('XDG_CONFIG_HOME') or os.path.join('$HOME', '.config')
|
||||||
os.getenv('XDG_CONFIG_HOME') or os.path.join('$HOME', '.config')
|
if expand_home:
|
||||||
)
|
user_config_directory = os.path.expandvars(user_config_directory)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'/etc/borgmatic/config.yaml',
|
'/etc/borgmatic/config.yaml',
|
||||||
@@ -47,6 +50,5 @@ def collect_config_filenames(config_paths):
|
|||||||
for filename in sorted(os.listdir(path)):
|
for filename in sorted(os.listdir(path)):
|
||||||
full_filename = os.path.join(path, filename)
|
full_filename = os.path.join(path, filename)
|
||||||
matching_filetype = full_filename.endswith(('.yaml', '.yml'))
|
matching_filetype = full_filename.endswith(('.yaml', '.yml'))
|
||||||
|
|
||||||
if matching_filetype and not os.path.isdir(full_filename):
|
if matching_filetype and not os.path.isdir(full_filename):
|
||||||
yield os.path.abspath(full_filename)
|
yield os.path.abspath(full_filename)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import functools
|
|
||||||
import re
|
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
|
|
||||||
@@ -23,44 +21,7 @@ def coerce_scalar(value):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
CONSTANT_PATTERN = re.compile(r'(?P<left_escape>\\)?\{(?P<name>[\w]+)(?P<right_escape>\\)?\}')
|
def apply_constants(value, constants, shell_escape=False):
|
||||||
|
|
||||||
|
|
||||||
def resolve_constant(match, constants, command_hook):
|
|
||||||
'''
|
|
||||||
Given a re.Match instance of CONSTANT_PATTERN representing a matched constant name to be
|
|
||||||
interpolated, a constants dict, and whether this is for a command hook, lookup the matched
|
|
||||||
constant name within the given constants and return its value.
|
|
||||||
|
|
||||||
If the match is escaped with backslashes, then instead of resolving the variable's value, strip
|
|
||||||
off the backslashing and return the literal value.
|
|
||||||
|
|
||||||
If the variable name isn't found in the given constants, then return the literal value.
|
|
||||||
'''
|
|
||||||
name = match.group('name')
|
|
||||||
|
|
||||||
# The would-be variable is escaped, so strip off the escaping and return the result without
|
|
||||||
# resolving the name—unless this is for a command hook, in which case just return the literal
|
|
||||||
# string. That way, subsequent variable interpolation will still see the string as escaped
|
|
||||||
# instead of trying to interpolate it.
|
|
||||||
if match.group('left_escape') and match.group('right_escape'):
|
|
||||||
if command_hook:
|
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
return '{' + name + '}'
|
|
||||||
|
|
||||||
value = constants.get(name)
|
|
||||||
|
|
||||||
# The matched variable is in the constants, so return its value. And if this is for a command
|
|
||||||
# hook, then shell escape the value so as to prevent shell injection attacks.
|
|
||||||
if value is not None:
|
|
||||||
return shlex.quote(str(value)) if command_hook else str(value)
|
|
||||||
|
|
||||||
# The matched variable name isn't in the constants. Return the whole string unaltered.
|
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_constants(value, constants, command_hook=False):
|
|
||||||
'''
|
'''
|
||||||
Given a configuration value (bool, dict, int, list, or string) and a dict of named constants,
|
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
|
replace any configuration string values of the form "{constant}" (or containing it) with the
|
||||||
@@ -78,25 +39,26 @@ def apply_constants(value, constants, command_hook=False):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
# Support constants within non-string scalars by coercing the value to its appropriate type.
|
for constant_name, constant_value in constants.items():
|
||||||
value = coerce_scalar(
|
value = value.replace(
|
||||||
CONSTANT_PATTERN.sub(
|
'{' + constant_name + '}',
|
||||||
functools.partial(resolve_constant, constants=constants, command_hook=command_hook),
|
shlex.quote(str(constant_value)) if shell_escape else str(constant_value),
|
||||||
value,
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
# Support constants within non-string scalars by coercing the value to its appropriate type.
|
||||||
|
value = coerce_scalar(value)
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, list):
|
||||||
for index, list_value in enumerate(value):
|
for index, list_value in enumerate(value):
|
||||||
value[index] = apply_constants(list_value, constants, command_hook)
|
value[index] = apply_constants(list_value, constants, shell_escape)
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
for option_name, option_value in value.items():
|
for option_name, option_value in value.items():
|
||||||
value[option_name] = apply_constants(
|
value[option_name] = apply_constants(
|
||||||
option_value,
|
option_value,
|
||||||
constants,
|
constants,
|
||||||
command_hook=(
|
shell_escape=(
|
||||||
command_hook
|
shell_escape
|
||||||
or option_name.startswith(('before_', 'after_'))
|
or option_name.startswith(('before_', 'after_'))
|
||||||
or option_name in {'on_error', 'run'}
|
or option_name == 'on_error'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -108,7 +107,7 @@ def comment_out_line(line):
|
|||||||
return '# '.join((indent_spaces, line[count_indent_spaces:]))
|
return '# '.join((indent_spaces, line[count_indent_spaces:]))
|
||||||
|
|
||||||
|
|
||||||
def transform_optional_configuration(rendered_config, comment_out=True):
|
def comment_out_optional_configuration(rendered_config):
|
||||||
'''
|
'''
|
||||||
Post-process a rendered configuration string to comment out optional key/values, as determined
|
Post-process a rendered configuration string to comment out optional key/values, as determined
|
||||||
by a sentinel in the comment before each key.
|
by a sentinel in the comment before each key.
|
||||||
@@ -118,9 +117,6 @@ def transform_optional_configuration(rendered_config, comment_out=True):
|
|||||||
|
|
||||||
Ideally ruamel.yaml would support commenting out keys during configuration generation, but it's
|
Ideally ruamel.yaml would support commenting out keys during configuration generation, but it's
|
||||||
not terribly easy to accomplish that way.
|
not terribly easy to accomplish that way.
|
||||||
|
|
||||||
If comment_out is False, then just strip the comment sentinel without actually commenting
|
|
||||||
anything out.
|
|
||||||
'''
|
'''
|
||||||
lines = []
|
lines = []
|
||||||
optional = False
|
optional = False
|
||||||
@@ -133,9 +129,6 @@ def transform_optional_configuration(rendered_config, comment_out=True):
|
|||||||
# Upon encountering an optional configuration option, comment out lines until the next blank
|
# Upon encountering an optional configuration option, comment out lines until the next blank
|
||||||
# line.
|
# line.
|
||||||
if line.strip().startswith(f'# {COMMENTED_OUT_SENTINEL}'):
|
if line.strip().startswith(f'# {COMMENTED_OUT_SENTINEL}'):
|
||||||
if comment_out is False:
|
|
||||||
continue
|
|
||||||
|
|
||||||
optional = True
|
optional = True
|
||||||
indent_characters_at_sentinel = indent_characters
|
indent_characters_at_sentinel = indent_characters
|
||||||
continue
|
continue
|
||||||
@@ -153,9 +146,6 @@ def transform_optional_configuration(rendered_config, comment_out=True):
|
|||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
|
||||||
RUAMEL_YAML_END_OF_DOCUMENT_MARKER = '...\n'
|
|
||||||
|
|
||||||
|
|
||||||
def render_configuration(config):
|
def render_configuration(config):
|
||||||
'''
|
'''
|
||||||
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
|
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
|
||||||
@@ -163,13 +153,7 @@ def render_configuration(config):
|
|||||||
dumper = ruamel.yaml.YAML(typ='rt')
|
dumper = ruamel.yaml.YAML(typ='rt')
|
||||||
dumper.indent(mapping=INDENT, sequence=INDENT + SEQUENCE_INDENT, offset=INDENT)
|
dumper.indent(mapping=INDENT, sequence=INDENT + SEQUENCE_INDENT, offset=INDENT)
|
||||||
rendered = io.StringIO()
|
rendered = io.StringIO()
|
||||||
dumper.dump(
|
dumper.dump(config, rendered)
|
||||||
config,
|
|
||||||
rendered,
|
|
||||||
# Dumping certain values (integers, for instance) causes ruamel.yaml to append an
|
|
||||||
# end-of-document "..." marker. Strip it.
|
|
||||||
transform=lambda dumped: dumped.removesuffix(RUAMEL_YAML_END_OF_DOCUMENT_MARKER),
|
|
||||||
)
|
|
||||||
|
|
||||||
return rendered.getvalue()
|
return rendered.getvalue()
|
||||||
|
|
||||||
@@ -326,39 +310,19 @@ def merge_source_configuration_into_destination(destination_config, source_confi
|
|||||||
return destination_config
|
return destination_config
|
||||||
|
|
||||||
|
|
||||||
def get_configuration_subset(config, option_name): # pragma: no cover
|
|
||||||
'''
|
|
||||||
Given configuration as a ruamel.yaml.CommentedMap and an option name found within it at the top
|
|
||||||
level, return a new CommentedMap containing a subset of the configuration with only the given
|
|
||||||
option and no other top-level options.
|
|
||||||
|
|
||||||
This is useful when generating the sample configuration for a single option instead of a whole
|
|
||||||
configuration file.
|
|
||||||
'''
|
|
||||||
option_config = ruamel.yaml.CommentedMap({option_name: config[option_name]})
|
|
||||||
|
|
||||||
# Due to a quirk of ruamel.yaml, the comment right before a top-level key is not on that key and
|
|
||||||
# needs to get copied separately.
|
|
||||||
option_config.ca.items[option_name] = config.ca.items[option_name]
|
|
||||||
|
|
||||||
return option_config
|
|
||||||
|
|
||||||
|
|
||||||
def generate_sample_configuration(
|
def generate_sample_configuration(
|
||||||
dry_run,
|
dry_run,
|
||||||
source_filename,
|
source_filename,
|
||||||
destination_path,
|
destination_filename,
|
||||||
schema_filename,
|
schema_filename,
|
||||||
overwrite=False,
|
overwrite=False,
|
||||||
split=False,
|
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Given an optional source configuration filename, a required destination configuration path, the
|
Given an optional source configuration filename, and a required destination configuration
|
||||||
path to a schema filename in a YAML rendition of the JSON Schema format, whether to overwrite a
|
filename, the path to a schema filename in a YAML rendition of the JSON Schema format, and
|
||||||
destination file, and whether to split the configuration into multiple files (one per option) in
|
whether to overwrite a destination file, write out a sample configuration file based on that
|
||||||
the assumed destination directory, write out sample configuration file(s) based on that schema.
|
schema. If a source filename is provided, merge the parsed contents of that configuration into
|
||||||
If a source filename is provided, merge the parsed contents of that configuration into the
|
the generated configuration.
|
||||||
generated configuration.
|
|
||||||
'''
|
'''
|
||||||
schema = ruamel.yaml.YAML(typ='safe').load(open(schema_filename, encoding='utf-8'))
|
schema = ruamel.yaml.YAML(typ='safe').load(open(schema_filename, encoding='utf-8'))
|
||||||
source_config = None
|
source_config = None
|
||||||
@@ -381,37 +345,8 @@ def generate_sample_configuration(
|
|||||||
if dry_run:
|
if dry_run:
|
||||||
return
|
return
|
||||||
|
|
||||||
if split:
|
|
||||||
if os.path.exists(destination_path) and not os.path.isdir(destination_path):
|
|
||||||
raise ValueError('With the --split flag, the destination path must be a directory')
|
|
||||||
|
|
||||||
os.makedirs(destination_path, exist_ok=True)
|
|
||||||
|
|
||||||
for option_name in destination_config:
|
|
||||||
write_configuration(
|
|
||||||
os.path.join(destination_path, f'{option_name}.yaml'),
|
|
||||||
transform_optional_configuration(
|
|
||||||
render_configuration(get_configuration_subset(destination_config, option_name)),
|
|
||||||
comment_out=False,
|
|
||||||
).strip(),
|
|
||||||
overwrite=overwrite,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Also dump a manifest listing all the options we've written.
|
|
||||||
json.dump(
|
|
||||||
{'option_names': list(destination_config.keys())},
|
|
||||||
open(os.path.join(destination_path, 'options.json'), 'w', encoding='utf-8'),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
if os.path.exists(destination_path) and not os.path.isfile(destination_path):
|
|
||||||
raise ValueError('Without the --split flag, the destination path must be a file')
|
|
||||||
|
|
||||||
write_configuration(
|
write_configuration(
|
||||||
destination_path,
|
destination_filename,
|
||||||
transform_optional_configuration(
|
comment_out_optional_configuration(render_configuration(destination_config)),
|
||||||
render_configuration(destination_config), comment_out=True
|
|
||||||
),
|
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
)
|
)
|
||||||
|
|||||||
+22
-65
@@ -1,21 +1,11 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Systemd_directories(Enum):
|
|
||||||
RUNTIME_DIRECTORY = 0
|
|
||||||
STATE_DIRECTORY = 1
|
|
||||||
CACHE_DIRECTORY = 2
|
|
||||||
LOGS_DIRECTORY = 3
|
|
||||||
CONFIGURATION_DIRECTORY = 4
|
|
||||||
|
|
||||||
|
|
||||||
def expand_user_in_path(path):
|
def expand_user_in_path(path):
|
||||||
'''
|
'''
|
||||||
Given a directory path, expand any tildes in it.
|
Given a directory path, expand any tildes in it.
|
||||||
@@ -26,17 +16,6 @@ def expand_user_in_path(path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_systemd_directory(directory):
|
|
||||||
'''
|
|
||||||
Given a systemd directory environment variable enum, read the value if set and return the first
|
|
||||||
configured directory.
|
|
||||||
'''
|
|
||||||
separator = ':'
|
|
||||||
|
|
||||||
paths = os.environ.get(directory.name)
|
|
||||||
return paths.split(separator)[0] if paths else None
|
|
||||||
|
|
||||||
|
|
||||||
def get_working_directory(config): # pragma: no cover
|
def get_working_directory(config): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given a configuration dict, get the working directory from it, expanding any tildes.
|
Given a configuration dict, get the working directory from it, expanding any tildes.
|
||||||
@@ -85,25 +64,6 @@ def replace_temporary_subdirectory_with_glob(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Fixed_name_temporary_directory:
|
|
||||||
'''
|
|
||||||
A class whose instances can stand-in for tempfile.TemporaryDirectory's, except the temporary
|
|
||||||
filename path is fixed rather than randomly generated.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, path):
|
|
||||||
'''
|
|
||||||
Given a temporary directory path, save it off for later.
|
|
||||||
'''
|
|
||||||
self.name = path
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
'''
|
|
||||||
Remove the temporary directory path.
|
|
||||||
'''
|
|
||||||
shutil.rmtree(self.name, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Runtime_directory:
|
class Runtime_directory:
|
||||||
'''
|
'''
|
||||||
A Python context manager for creating and cleaning up the borgmatic runtime directory used for
|
A Python context manager for creating and cleaning up the borgmatic runtime directory used for
|
||||||
@@ -118,17 +78,16 @@ class Runtime_directory:
|
|||||||
automatically gets cleaned up as necessary.
|
automatically gets cleaned up as necessary.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, config, repository_id):
|
def __init__(self, config):
|
||||||
'''
|
'''
|
||||||
Given a configuration dict and the Borg ID for a repository, determine the borgmatic runtime
|
Given a configuration dict determine the borgmatic runtime directory, creating a secure,
|
||||||
directory, creating a secure, temporary directory within it if necessary. Defaults to
|
temporary directory within it if necessary. Defaults to $XDG_RUNTIME_DIR/./borgmatic or
|
||||||
$XDG_RUNTIME_DIR/borgmatic-[repository_id]/./borgmatic or
|
$RUNTIME_DIRECTORY/./borgmatic or $TMPDIR/borgmatic-[random]/./borgmatic or
|
||||||
$RUNTIME_DIRECTORY/borgmatic-[repository_id]/./borgmatic or
|
$TEMP/borgmatic-[random]/./borgmatic or /tmp/borgmatic-[random]/./borgmatic where "[random]"
|
||||||
$TMPDIR/borgmatic-[random]/./borgmatic or $TEMP/borgmatic-[random]/./borgmatic or
|
is a randomly generated string intended to avoid path collisions.
|
||||||
/tmp/borgmatic-[random]/./borgmatic where "[random]" is a randomly generated string and
|
|
||||||
"[repository_id]" is the Borg repository ID being operated on. Both strings are intended to
|
If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't
|
||||||
avoid path collisions, and the random string helps avoid predictable temporary path attacks
|
tack on a second "/borgmatic" path component.
|
||||||
in shared temporary directories.
|
|
||||||
|
|
||||||
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
|
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
|
||||||
does not get stored in the file path within an archive. That way, the path of the runtime
|
does not get stored in the file path within an archive. That way, the path of the runtime
|
||||||
@@ -137,17 +96,14 @@ class Runtime_directory:
|
|||||||
runtime_directory = (
|
runtime_directory = (
|
||||||
config.get('user_runtime_directory')
|
config.get('user_runtime_directory')
|
||||||
or os.environ.get('XDG_RUNTIME_DIR') # Set by PAM on Linux.
|
or os.environ.get('XDG_RUNTIME_DIR') # Set by PAM on Linux.
|
||||||
or resolve_systemd_directory(
|
or os.environ.get('RUNTIME_DIRECTORY') # Set by systemd if configured.
|
||||||
Systemd_directories.RUNTIME_DIRECTORY
|
|
||||||
) # Set by systemd if configured.
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if runtime_directory:
|
if runtime_directory:
|
||||||
if not runtime_directory.startswith(os.path.sep):
|
if not runtime_directory.startswith(os.path.sep):
|
||||||
raise ValueError('The runtime directory must be an absolute path')
|
raise ValueError('The runtime directory must be an absolute path')
|
||||||
|
|
||||||
runtime_directory = os.path.join(runtime_directory, f'borgmatic-{repository_id}')
|
self.temporary_directory = None
|
||||||
self.temporary_directory = Fixed_name_temporary_directory(runtime_directory)
|
|
||||||
else:
|
else:
|
||||||
base_directory = (
|
base_directory = (
|
||||||
os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp' # noqa: S108
|
os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp' # noqa: S108
|
||||||
@@ -163,9 +119,11 @@ class Runtime_directory:
|
|||||||
)
|
)
|
||||||
runtime_directory = self.temporary_directory.name
|
runtime_directory = self.temporary_directory.name
|
||||||
|
|
||||||
|
(base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep))
|
||||||
|
|
||||||
self.runtime_path = expand_user_in_path(
|
self.runtime_path = expand_user_in_path(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
runtime_directory,
|
base_path if final_directory == 'borgmatic' else runtime_directory,
|
||||||
'.', # Borg 1.4+ "slashdot" hack.
|
'.', # Borg 1.4+ "slashdot" hack.
|
||||||
'borgmatic',
|
'borgmatic',
|
||||||
),
|
),
|
||||||
@@ -182,13 +140,14 @@ class Runtime_directory:
|
|||||||
|
|
||||||
def __exit__(self, exception_type, exception, traceback):
|
def __exit__(self, exception_type, exception, traceback):
|
||||||
'''
|
'''
|
||||||
Delete the temporary directory that was created as part of initialization.
|
Delete any temporary directory that was created as part of initialization.
|
||||||
'''
|
'''
|
||||||
# The cleanup() call errors if, for instance, there's still a
|
if self.temporary_directory:
|
||||||
# mounted filesystem within the temporary directory. There's
|
# The cleanup() call errors if, for instance, there's still a
|
||||||
# nothing we can do about that here, so swallow the error.
|
# mounted filesystem within the temporary directory. There's
|
||||||
with contextlib.suppress(OSError):
|
# nothing we can do about that here, so swallow the error.
|
||||||
self.temporary_directory.cleanup()
|
with contextlib.suppress(OSError):
|
||||||
|
self.temporary_directory.cleanup()
|
||||||
|
|
||||||
|
|
||||||
def make_runtime_directory_glob(borgmatic_runtime_directory):
|
def make_runtime_directory_glob(borgmatic_runtime_directory):
|
||||||
@@ -215,9 +174,7 @@ def get_borgmatic_state_directory(config):
|
|||||||
os.path.join(
|
os.path.join(
|
||||||
config.get('user_state_directory')
|
config.get('user_state_directory')
|
||||||
or os.environ.get('XDG_STATE_HOME')
|
or os.environ.get('XDG_STATE_HOME')
|
||||||
or resolve_systemd_directory(
|
or os.environ.get('STATE_DIRECTORY') # Set by systemd if configured.
|
||||||
Systemd_directories.STATE_DIRECTORY
|
|
||||||
) # Set by systemd if configured.
|
|
||||||
or '~/.local/state',
|
or '~/.local/state',
|
||||||
'borgmatic',
|
'borgmatic',
|
||||||
),
|
),
|
||||||
|
|||||||
+89
-346
@@ -27,13 +27,6 @@ properties:
|
|||||||
- /etc
|
- /etc
|
||||||
- /var/log/syslog*
|
- /var/log/syslog*
|
||||||
- /home/user/path with spaces
|
- /home/user/path with spaces
|
||||||
source_directories_must_exist:
|
|
||||||
type: boolean
|
|
||||||
description: |
|
|
||||||
When true, source directories (and root pattern paths) must exist
|
|
||||||
before a backup begins. If they don't, borgmatic errors. Defaults to
|
|
||||||
true.
|
|
||||||
example: false
|
|
||||||
repositories:
|
repositories:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -80,7 +73,7 @@ properties:
|
|||||||
Whether any missing parent directories of the repository
|
Whether any missing parent directories of the repository
|
||||||
path should be created, only used for the repo-create
|
path should be created, only used for the repo-create
|
||||||
action. Defaults to false. (This option is supported
|
action. Defaults to false. (This option is supported
|
||||||
for Borg 1.x only.)
|
for Borg 1.x only)
|
||||||
example: true
|
example: true
|
||||||
description: |
|
description: |
|
||||||
A required list of local or remote repositories with paths and
|
A required list of local or remote repositories with paths and
|
||||||
@@ -141,39 +134,12 @@ properties:
|
|||||||
false. But when a database hook is used, the setting here is ignored
|
false. But when a database hook is used, the setting here is ignored
|
||||||
and read_special is considered true.
|
and read_special is considered true.
|
||||||
example: true
|
example: true
|
||||||
unsafe_skip_path_validation_before_create:
|
|
||||||
type: boolean
|
|
||||||
description: |
|
|
||||||
Skip pre-backup safety validation: (1) runtime directory exclusion
|
|
||||||
detection, and (2) special file exclusion for database streaming.
|
|
||||||
Can significantly improve performance for large filesystems.
|
|
||||||
|
|
||||||
UNSAFE because, when this option is enabled, Borg may hang on
|
|
||||||
special files (named pipes, devices) and data from borgmatic data
|
|
||||||
source hooks may be silently excluded if your excluded patterns
|
|
||||||
match the runtime directory (see the "user_runtime_directory"
|
|
||||||
option).
|
|
||||||
|
|
||||||
Only enable if not using data source hooks and you manually verify
|
|
||||||
that your excludes don't affect the runtime directory. Defaults
|
|
||||||
to false.
|
|
||||||
example: true
|
|
||||||
flags:
|
flags:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive.
|
Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive.
|
||||||
Defaults to true.
|
Defaults to true.
|
||||||
example: false
|
example: false
|
||||||
files_changed:
|
|
||||||
type: string
|
|
||||||
enum: ['ctime', 'mtime', 'disabled']
|
|
||||||
description: |
|
|
||||||
Threshold for considering a file as changed. See
|
|
||||||
https://borgbackup.readthedocs.io/en/stable/usage/create.html for
|
|
||||||
details. Defaults to "ctime". E.g., a file is considered changed if
|
|
||||||
its ctime has changed since the last backup. (This option is
|
|
||||||
supported for Borg 1.4.2+ only.)
|
|
||||||
example: ctime
|
|
||||||
files_cache:
|
files_cache:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
@@ -197,13 +163,9 @@ properties:
|
|||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Any paths matching these patterns are included/excluded from
|
Any paths matching these patterns are included/excluded from
|
||||||
backups. Recursion root patterns ("R ...") are effectively the same
|
backups. Globs are expanded. (Tildes are not.) See the output of
|
||||||
as "source_directories"; they tell Borg which paths to backup
|
"borg help patterns" for more details. Quote any value if it
|
||||||
(modulo any excludes). Globs are expanded. (Tildes are not.) See
|
contains leading punctuation, so it parses correctly.
|
||||||
the output of "borg help patterns" for more details. Quote any value
|
|
||||||
if it contains leading punctuation, so it parses correctly. Also use
|
|
||||||
leading slashes in absolute paths, or data source hooks may be
|
|
||||||
unable to rewrite patterns as needed.
|
|
||||||
example:
|
example:
|
||||||
- 'R /'
|
- 'R /'
|
||||||
- '- /home/*/.cache'
|
- '- /home/*/.cache'
|
||||||
@@ -270,8 +232,7 @@ properties:
|
|||||||
exclude_nodump:
|
exclude_nodump:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Exclude files with the NODUMP flag. Defaults to false. (This option
|
Exclude files with the NODUMP flag. Defaults to false.
|
||||||
is supported for Borg 1.x only.)
|
|
||||||
example: true
|
example: true
|
||||||
borgmatic_source_directory:
|
borgmatic_source_directory:
|
||||||
type: string
|
type: string
|
||||||
@@ -287,7 +248,7 @@ properties:
|
|||||||
Path for storing temporary runtime data like streaming database
|
Path for storing temporary runtime data like streaming database
|
||||||
dumps and bootstrap metadata. borgmatic automatically creates and
|
dumps and bootstrap metadata. borgmatic automatically creates and
|
||||||
uses a "borgmatic" subdirectory here. Defaults to $XDG_RUNTIME_DIR
|
uses a "borgmatic" subdirectory here. Defaults to $XDG_RUNTIME_DIR
|
||||||
or $TMPDIR or $TEMP or /run/user/$UID.
|
or or $TMPDIR or $TEMP or /run/user/$UID.
|
||||||
example: /run/user/1001
|
example: /run/user/1001
|
||||||
user_state_directory:
|
user_state_directory:
|
||||||
type: string
|
type: string
|
||||||
@@ -298,6 +259,12 @@ properties:
|
|||||||
create the check records again (and therefore re-run checks).
|
create the check records again (and therefore re-run checks).
|
||||||
Defaults to $XDG_STATE_HOME or ~/.local/state.
|
Defaults to $XDG_STATE_HOME or ~/.local/state.
|
||||||
example: /var/lib/borgmatic
|
example: /var/lib/borgmatic
|
||||||
|
source_directories_must_exist:
|
||||||
|
type: boolean
|
||||||
|
description: |
|
||||||
|
If true, then source directories (and root pattern paths) must
|
||||||
|
exist. If they don't, an error is raised. Defaults to false.
|
||||||
|
example: true
|
||||||
encryption_passcommand:
|
encryption_passcommand:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
@@ -308,7 +275,7 @@ properties:
|
|||||||
encryption_passphrase takes precedence. This can also be used to
|
encryption_passphrase takes precedence. This can also be used to
|
||||||
access encrypted systemd service credentials. Defaults to not set.
|
access encrypted systemd service credentials. Defaults to not set.
|
||||||
For more details, see:
|
For more details, see:
|
||||||
https://torsion.org/borgmatic/how-to/provide-your-passwords/
|
https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
|
||||||
example: "secret-tool lookup borg-repository repo-name"
|
example: "secret-tool lookup borg-repository repo-name"
|
||||||
encryption_passphrase:
|
encryption_passphrase:
|
||||||
type: string
|
type: string
|
||||||
@@ -439,20 +406,14 @@ properties:
|
|||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Path for Borg security and encryption nonce files. Defaults to
|
Path for Borg security and encryption nonce files. Defaults to
|
||||||
$borg_config_directory/security
|
$borg_base_directory/.config/borg/security
|
||||||
example: /path/to/base/config/security
|
example: /path/to/base/config/security
|
||||||
borg_keys_directory:
|
borg_keys_directory:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Path for Borg encryption key files. Defaults to
|
Path for Borg encryption key files. Defaults to
|
||||||
$borg_config_directory/keys
|
$borg_base_directory/.config/borg/keys
|
||||||
example: /path/to/base/config/keys
|
example: /path/to/base/config/keys
|
||||||
borg_key_file:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Path for the Borg repository key file, for use with a repository
|
|
||||||
created with "keyfile" encryption.
|
|
||||||
example: /path/to/base/config/keyfile
|
|
||||||
borg_exit_codes:
|
borg_exit_codes:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -476,9 +437,9 @@ properties:
|
|||||||
description: |
|
description: |
|
||||||
A list of Borg exit codes that should be elevated to errors or
|
A list of Borg exit codes that should be elevated to errors or
|
||||||
squashed to warnings as indicated. By default, Borg error exit codes
|
squashed to warnings as indicated. By default, Borg error exit codes
|
||||||
(2 to 99) are treated as errors and most warning exit codes (1 and
|
(2 to 99) are treated as errors while warning exit codes (1 and
|
||||||
100+) are treated as errors as well. Exit codes other than 1 and 2
|
100+) are treated as warnings. Exit codes other than 1 and 2 are
|
||||||
are only present in Borg 1.4.0+.
|
only present in Borg 1.4.0+.
|
||||||
example:
|
example:
|
||||||
- code: 13
|
- code: 13
|
||||||
treat_as: warning
|
treat_as: warning
|
||||||
@@ -489,9 +450,6 @@ properties:
|
|||||||
description: |
|
description: |
|
||||||
Umask used for when executing Borg or calling hooks. Defaults to
|
Umask used for when executing Borg or calling hooks. Defaults to
|
||||||
0077 for Borg or the umask that borgmatic is run with for hooks.
|
0077 for Borg or the umask that borgmatic is run with for hooks.
|
||||||
Even though this value is a YAML integer, borgmatic interprets it as
|
|
||||||
octal. YAML's "0o"-prefixed octal notation is not currently
|
|
||||||
supported.
|
|
||||||
example: 0077
|
example: 0077
|
||||||
lock_wait:
|
lock_wait:
|
||||||
type: integer
|
type: integer
|
||||||
@@ -521,27 +479,6 @@ properties:
|
|||||||
If match_archives is not specified, borgmatic defaults to deriving
|
If match_archives is not specified, borgmatic defaults to deriving
|
||||||
the match_archives value from archive_name_format.
|
the match_archives value from archive_name_format.
|
||||||
example: "sh:{hostname}-*"
|
example: "sh:{hostname}-*"
|
||||||
file_list_format:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Borg format for the files listing of the "list" action. Corresponds
|
|
||||||
to the "--format" flag. Defaults to "{mode} {user:6} {group:6}
|
|
||||||
{size:8} {mtime} {path}{extra}{NL}". With "--json", the form of the
|
|
||||||
format is ignored, but the keys used in it are added to the JSON
|
|
||||||
output. See
|
|
||||||
https://borgbackup.readthedocs.io/en/stable/usage/list.html for
|
|
||||||
details.
|
|
||||||
example: "{path}{extra}{NL}"
|
|
||||||
archive_list_format:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Borg format for the archives listing of the "repo-list" action.
|
|
||||||
Corresponds to the "--format" flag. Defaults to "{archive:<36}
|
|
||||||
{time} [{id}]{NL}". With "--json", the form of the format is
|
|
||||||
ignored, but the keys used in it are added to the JSON output. See
|
|
||||||
https://borgbackup.readthedocs.io/en/stable/usage/list.html for
|
|
||||||
details.
|
|
||||||
example: "{archive}{NL}"
|
|
||||||
relocated_repo_access_is_ok:
|
relocated_repo_access_is_ok:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
@@ -573,133 +510,34 @@ properties:
|
|||||||
Bypass Borg confirmation about check with repair option. Defaults to
|
Bypass Borg confirmation about check with repair option. Defaults to
|
||||||
false and an interactive prompt from Borg.
|
false and an interactive prompt from Borg.
|
||||||
example: true
|
example: true
|
||||||
msgpack_version_check:
|
|
||||||
type: boolean
|
|
||||||
description: |
|
|
||||||
Optionally disable the msgpack version check. Default is true; use
|
|
||||||
at your own risk. (This option is supported for Borg 1.4.2+ only.)
|
|
||||||
example: false
|
|
||||||
extra_borg_options:
|
extra_borg_options:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
properties:
|
properties:
|
||||||
break_lock:
|
init:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Extra command-line options to pass to "borg break-lock".
|
Extra command-line options to pass to "borg init".
|
||||||
example: "--extra-option"
|
|
||||||
check:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg check".
|
|
||||||
example: "--extra-option"
|
|
||||||
compact:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg compact".
|
|
||||||
example: "--extra-option"
|
example: "--extra-option"
|
||||||
create:
|
create:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Extra command-line options to pass to "borg create".
|
Extra command-line options to pass to "borg create".
|
||||||
example: "--extra-option"
|
example: "--extra-option"
|
||||||
delete:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg delete".
|
|
||||||
example: "--extra-option"
|
|
||||||
export_tar:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg export-tar".
|
|
||||||
example: "--extra-option"
|
|
||||||
extract:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg extract".
|
|
||||||
example: "--extra-option"
|
|
||||||
key_export:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg key export".
|
|
||||||
example: "--extra-option"
|
|
||||||
key_import:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg key import".
|
|
||||||
example: "--extra-option"
|
|
||||||
key_change_passphrase:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg key
|
|
||||||
change-passphrase".
|
|
||||||
example: "--extra-option"
|
|
||||||
info:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg info".
|
|
||||||
example: "--extra-option"
|
|
||||||
init:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Deprecated. Use "repo_create" instead. Extra command-line
|
|
||||||
options to pass to "borg init" / "borg repo-create".
|
|
||||||
example: "--extra-option"
|
|
||||||
list:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg list".
|
|
||||||
example: "--extra-option"
|
|
||||||
mount:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg mount".
|
|
||||||
example: "--extra-option"
|
|
||||||
prune:
|
prune:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Extra command-line options to pass to "borg prune".
|
Extra command-line options to pass to "borg prune".
|
||||||
example: "--extra-option"
|
example: "--extra-option"
|
||||||
recreate:
|
compact:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Extra command-line options to pass to "borg recreate".
|
Extra command-line options to pass to "borg compact".
|
||||||
example: "--extra-option"
|
example: "--extra-option"
|
||||||
rename:
|
check:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Extra command-line options to pass to "borg rename".
|
Extra command-line options to pass to "borg check".
|
||||||
example: "--extra-option"
|
|
||||||
repo_create:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg init" / "borg
|
|
||||||
repo-create".
|
|
||||||
example: "--extra-option"
|
|
||||||
repo_delete:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg repo-delete".
|
|
||||||
example: "--extra-option"
|
|
||||||
repo_info:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg repo-info".
|
|
||||||
example: "--extra-option"
|
|
||||||
repo_list:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg repo-list".
|
|
||||||
example: "--extra-option"
|
|
||||||
transfer:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg transfer".
|
|
||||||
example: "--extra-option"
|
|
||||||
umount:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Extra command-line options to pass to "borg umount".
|
|
||||||
example: "--extra-option"
|
example: "--extra-option"
|
||||||
description: |
|
description: |
|
||||||
Additional options to pass directly to particular Borg commands,
|
Additional options to pass directly to particular Borg commands,
|
||||||
@@ -1054,8 +892,6 @@ properties:
|
|||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Python format string used for log messages written to the log file.
|
Python format string used for log messages written to the log file.
|
||||||
See https://docs.python.org/3/library/logging.html (and specifically
|
|
||||||
the LogRecord attributes with "{}-formatting") for details.
|
|
||||||
example: "[{asctime}] {levelname}: {prefix}{message}"
|
example: "[{asctime}] {levelname}: {prefix}{message}"
|
||||||
monitoring_verbosity:
|
monitoring_verbosity:
|
||||||
type: integer
|
type: integer
|
||||||
@@ -1101,9 +937,9 @@ properties:
|
|||||||
default_actions:
|
default_actions:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Whether to run default actions (create, prune, compact, and check)
|
Whether to apply default actions (create, prune, compact and check)
|
||||||
when no arguments are given on the command line. If set to false,
|
when no arguments are supplied to the borgmatic command. If set to
|
||||||
borgmatic displays the help message instead.
|
false, borgmatic displays the help message instead.
|
||||||
example: true
|
example: true
|
||||||
skip_actions:
|
skip_actions:
|
||||||
type: array
|
type: array
|
||||||
@@ -1132,7 +968,6 @@ properties:
|
|||||||
- break-lock
|
- break-lock
|
||||||
- key
|
- key
|
||||||
- borg
|
- borg
|
||||||
- diff
|
|
||||||
description: |
|
description: |
|
||||||
List of one or more actions to skip running for this configuration
|
List of one or more actions to skip running for this configuration
|
||||||
file, even if specified on the command-line (explicitly or
|
file, even if specified on the command-line (explicitly or
|
||||||
@@ -1305,6 +1140,7 @@ properties:
|
|||||||
before:
|
before:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
- step
|
||||||
- action
|
- action
|
||||||
- repository
|
- repository
|
||||||
- configuration
|
- configuration
|
||||||
@@ -1313,6 +1149,8 @@ properties:
|
|||||||
Name for the point in borgmatic's execution that
|
Name for the point in borgmatic's execution that
|
||||||
the commands should be run before (required if
|
the commands should be run before (required if
|
||||||
"after" isn't set):
|
"after" isn't set):
|
||||||
|
* "step" runs before a sub-action step for each
|
||||||
|
repository, e.g. for an individual check.
|
||||||
* "action" runs before each action for each
|
* "action" runs before each action for each
|
||||||
repository.
|
repository.
|
||||||
* "repository" runs before all actions for each
|
* "repository" runs before all actions for each
|
||||||
@@ -1349,11 +1187,22 @@ properties:
|
|||||||
- break-lock
|
- break-lock
|
||||||
- key
|
- key
|
||||||
- borg
|
- borg
|
||||||
- diff
|
|
||||||
description: |
|
description: |
|
||||||
List of actions for which the commands will be
|
List of actions for which the commands will be
|
||||||
run. Defaults to running for all actions.
|
run. Defaults to running for all actions.
|
||||||
example: [create, prune, compact, check]
|
example: [create, prune, compact, check]
|
||||||
|
steps:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- archives_repository_data
|
||||||
|
- extract
|
||||||
|
- spot
|
||||||
|
description: |
|
||||||
|
List of sub-action steps for which the commands
|
||||||
|
will be run. Defaults to running for all steps.
|
||||||
|
example: [extract, spot]
|
||||||
run:
|
run:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -1369,6 +1218,7 @@ properties:
|
|||||||
after:
|
after:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
- step
|
||||||
- action
|
- action
|
||||||
- repository
|
- repository
|
||||||
- configuration
|
- configuration
|
||||||
@@ -1378,6 +1228,8 @@ properties:
|
|||||||
Name for the point in borgmatic's execution that
|
Name for the point in borgmatic's execution that
|
||||||
the commands should be run after (required if
|
the commands should be run after (required if
|
||||||
"before" isn't set):
|
"before" isn't set):
|
||||||
|
* "step" runs before a sub-action step for each
|
||||||
|
repository, e.g. for an individual check.
|
||||||
* "action" runs after each action for each
|
* "action" runs after each action for each
|
||||||
repository.
|
repository.
|
||||||
* "repository" runs after all actions for each
|
* "repository" runs after all actions for each
|
||||||
@@ -1415,12 +1267,23 @@ properties:
|
|||||||
- break-lock
|
- break-lock
|
||||||
- key
|
- key
|
||||||
- borg
|
- borg
|
||||||
- diff
|
|
||||||
description: |
|
description: |
|
||||||
Only trigger the hook when borgmatic is run with
|
Only trigger the hook when borgmatic is run with
|
||||||
particular actions listed here. Defaults to
|
particular actions listed here. Defaults to
|
||||||
running for all actions.
|
running for all actions.
|
||||||
example: [create, prune, compact, check]
|
example: [create, prune, compact, check]
|
||||||
|
steps:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- archives_repository_data
|
||||||
|
- extract
|
||||||
|
- spot
|
||||||
|
description: |
|
||||||
|
List of sub-action steps for which the commands
|
||||||
|
will be run. Defaults to running for all steps.
|
||||||
|
example: [extract, spot]
|
||||||
states:
|
states:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -1708,15 +1571,6 @@ properties:
|
|||||||
database hook implicitly enables read_special (see
|
database hook implicitly enables read_special (see
|
||||||
above) to support dump and restore streaming.
|
above) to support dump and restore streaming.
|
||||||
example: users
|
example: users
|
||||||
skip_names:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Database names to skip when dumping "all" databases.
|
|
||||||
Ignored when the database name is not "all".
|
|
||||||
example:
|
|
||||||
- cache
|
|
||||||
label:
|
label:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
@@ -1755,20 +1609,6 @@ properties:
|
|||||||
description: |
|
description: |
|
||||||
Port to restore to. Defaults to the "port" option.
|
Port to restore to. Defaults to the "port" option.
|
||||||
example: 5433
|
example: 5433
|
||||||
socket_path:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Path of a Unix socket to connect to instead of a remote
|
|
||||||
hostname. Ignored when "hostname" is set and not
|
|
||||||
"localhost". Defaults to "/run/mysqld/mysqld.sock".
|
|
||||||
example: database.example.org
|
|
||||||
restore_socket_path:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Path of a Unix socket to connect to instead of a remote
|
|
||||||
hostname. Ignored when "hostname" is set and not
|
|
||||||
"localhost". Defaults to the "socket_path" option.
|
|
||||||
example: database.example.org
|
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
@@ -1832,21 +1672,18 @@ properties:
|
|||||||
mariadb_dump_command:
|
mariadb_dump_command:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Command to use instead of "mariadb-dump". But set any
|
Command to use instead of "mariadb-dump". This can be
|
||||||
additional command-line flags in "options", not here.
|
used to run a specific mariadb_dump version (e.g., one
|
||||||
This command can be used to run a specific mariadb_dump
|
inside a running container). If you run it from within a
|
||||||
version (e.g., one inside a running container). If you
|
container, make sure to mount the path in the
|
||||||
run it from within a container, make sure to mount the
|
"user_runtime_directory" option from the host into the
|
||||||
path in the "user_runtime_directory" option from the
|
container at the same location. Defaults to
|
||||||
host into the container at the same location. Defaults
|
"mariadb-dump".
|
||||||
to "mariadb-dump".
|
|
||||||
example: docker exec mariadb_container mariadb-dump
|
example: docker exec mariadb_container mariadb-dump
|
||||||
mariadb_command:
|
mariadb_command:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Command to run instead of "mariadb". But set any
|
Command to run instead of "mariadb". This can be used to
|
||||||
additional command-line flags in "list_options" or
|
|
||||||
"restore_options", not here. This command can be used to
|
|
||||||
run a specific mariadb version (e.g., one inside a
|
run a specific mariadb version (e.g., one inside a
|
||||||
running container). Defaults to "mariadb".
|
running container). Defaults to "mariadb".
|
||||||
example: docker exec mariadb_container mariadb
|
example: docker exec mariadb_container mariadb
|
||||||
@@ -1916,15 +1753,6 @@ properties:
|
|||||||
database hook implicitly enables read_special (see
|
database hook implicitly enables read_special (see
|
||||||
above) to support dump and restore streaming.
|
above) to support dump and restore streaming.
|
||||||
example: users
|
example: users
|
||||||
skip_names:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Database names to skip when dumping "all" databases.
|
|
||||||
Ignored when the database name is not "all".
|
|
||||||
example:
|
|
||||||
- cache
|
|
||||||
label:
|
label:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
@@ -1963,20 +1791,6 @@ properties:
|
|||||||
description: |
|
description: |
|
||||||
Port to restore to. Defaults to the "port" option.
|
Port to restore to. Defaults to the "port" option.
|
||||||
example: 5433
|
example: 5433
|
||||||
socket_path:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Path of a Unix socket to connect to instead of a remote
|
|
||||||
hostname. Ignored when "hostname" is set and not
|
|
||||||
"localhost". Defaults to "/run/mysqld/mysqld.sock".
|
|
||||||
example: database.example.org
|
|
||||||
restore_socket_path:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Path of a Unix socket to connect to instead of a remote
|
|
||||||
hostname. Ignored when "hostname" is set and not
|
|
||||||
"localhost". Defaults to the "socket_path" option.
|
|
||||||
example: database.example.org
|
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
@@ -2040,21 +1854,17 @@ properties:
|
|||||||
mysql_dump_command:
|
mysql_dump_command:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Command to use instead of "mysqldump". But set any
|
Command to use instead of "mysqldump". This can be used
|
||||||
additional command-line flags in "options", not here.
|
to run a specific mysql_dump version (e.g., one inside a
|
||||||
This command can be used to run a specific mysql_dump
|
running container). If you run it from within a
|
||||||
version (e.g., one inside a running container). If you
|
container, make sure to mount the path in the
|
||||||
run it from within a container, make sure to mount the
|
"user_runtime_directory" option from the host into the
|
||||||
path in the "user_runtime_directory" option from the
|
container at the same location. Defaults to "mysqldump".
|
||||||
host into the container at the same location. Defaults
|
|
||||||
to "mysqldump".
|
|
||||||
example: docker exec mysql_container mysqldump
|
example: docker exec mysql_container mysqldump
|
||||||
mysql_command:
|
mysql_command:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Command to run instead of "mysql". But set any
|
Command to run instead of "mysql". This can be used to
|
||||||
additional command-line flags in "list_options" or
|
|
||||||
"restore_options", not here. This command can be used to
|
|
||||||
run a specific mysql version (e.g., one inside a running
|
run a specific mysql version (e.g., one inside a running
|
||||||
container). Defaults to "mysql".
|
container). Defaults to "mysql".
|
||||||
example: docker exec mysql_container mysql
|
example: docker exec mysql_container mysql
|
||||||
@@ -2359,24 +2169,17 @@ properties:
|
|||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The message body to publish.
|
The message body to publish.
|
||||||
example: Your backups have started.
|
example: Your backups have failed.
|
||||||
priority:
|
priority:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
|
||||||
- max
|
|
||||||
- urgent
|
|
||||||
- high
|
|
||||||
- default
|
|
||||||
- low
|
|
||||||
- min
|
|
||||||
description: |
|
description: |
|
||||||
The priority to set.
|
The priority to set.
|
||||||
example: min
|
example: urgent
|
||||||
tags:
|
tags:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Tags to attach to the message.
|
Tags to attach to the message.
|
||||||
example: borgmatic
|
example: incoming_envelope
|
||||||
finish:
|
finish:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
@@ -2390,24 +2193,17 @@ properties:
|
|||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The message body to publish.
|
The message body to publish.
|
||||||
example: Your backups have finished.
|
example: Your backups have failed.
|
||||||
priority:
|
priority:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
|
||||||
- max
|
|
||||||
- urgent
|
|
||||||
- high
|
|
||||||
- default
|
|
||||||
- low
|
|
||||||
- min
|
|
||||||
description: |
|
description: |
|
||||||
The priority to set.
|
The priority to set.
|
||||||
example: min
|
example: urgent
|
||||||
tags:
|
tags:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Tags to attach to the message.
|
Tags to attach to the message.
|
||||||
example: borgmatic,+1
|
example: incoming_envelope
|
||||||
fail:
|
fail:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
@@ -2424,21 +2220,14 @@ properties:
|
|||||||
example: Your backups have failed.
|
example: Your backups have failed.
|
||||||
priority:
|
priority:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
|
||||||
- max
|
|
||||||
- urgent
|
|
||||||
- high
|
|
||||||
- default
|
|
||||||
- low
|
|
||||||
- min
|
|
||||||
description: |
|
description: |
|
||||||
The priority to set.
|
The priority to set.
|
||||||
example: max
|
example: urgent
|
||||||
tags:
|
tags:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Tags to attach to the message.
|
Tags to attach to the message.
|
||||||
example: borgmatic,-1,skull
|
example: incoming_envelope
|
||||||
states:
|
states:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -2847,8 +2636,8 @@ properties:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Send borgmatic logs to Apprise services as part of the
|
Send borgmatic logs to Apprise services as part of the
|
||||||
"finish", "fail", and "log" states. Defaults to false.
|
"finish", "fail", and "log" states. Defaults to true.
|
||||||
example: true
|
example: false
|
||||||
logs_size_limit:
|
logs_size_limit:
|
||||||
type: integer
|
type: integer
|
||||||
description: |
|
description: |
|
||||||
@@ -2962,8 +2751,8 @@ properties:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Send borgmatic logs to Healthchecks as part of the "finish",
|
Send borgmatic logs to Healthchecks as part of the "finish",
|
||||||
"fail", and "log" states. Defaults to false.
|
"fail", and "log" states. Defaults to true.
|
||||||
example: true
|
example: false
|
||||||
ping_body_limit:
|
ping_body_limit:
|
||||||
type: integer
|
type: integer
|
||||||
description: |
|
description: |
|
||||||
@@ -3069,8 +2858,8 @@ properties:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Send borgmatic logs to PagerDuty when a backup errors.
|
Send borgmatic logs to PagerDuty when a backup errors.
|
||||||
Defaults to false.
|
Defaults to true.
|
||||||
example: true
|
example: false
|
||||||
description: |
|
description: |
|
||||||
Configuration for a monitoring integration with PagerDuty. Create an
|
Configuration for a monitoring integration with PagerDuty. Create an
|
||||||
account at https://www.pagerduty.com if you'd like to use this
|
account at https://www.pagerduty.com if you'd like to use this
|
||||||
@@ -3098,27 +2887,9 @@ properties:
|
|||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Grafana Loki log URL to notify when a backup begins,
|
Grafana loki log URL to notify when a backup begins,
|
||||||
ends, or fails.
|
ends, or fails.
|
||||||
example: "http://localhost:3100/loki/api/v1/push"
|
example: "http://localhost:3100/loki/api/v1/push"
|
||||||
tls:
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
properties:
|
|
||||||
cert_path:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Path to a PEM client certificate file for mutual
|
|
||||||
TLS authentication.
|
|
||||||
example: /etc/borgmatic/loki-client.crt
|
|
||||||
key_path:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Path to a PEM private key file for the client
|
|
||||||
certificate.
|
|
||||||
example: /etc/borgmatic/loki-client.key
|
|
||||||
description: |
|
|
||||||
TLS options for mutual TLS (mTLS) authentication with Loki.
|
|
||||||
labels:
|
labels:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
@@ -3133,11 +2904,6 @@ properties:
|
|||||||
app: "borgmatic"
|
app: "borgmatic"
|
||||||
config: "__config"
|
config: "__config"
|
||||||
hostname: "__hostname"
|
hostname: "__hostname"
|
||||||
send_logs:
|
|
||||||
type: boolean
|
|
||||||
description: |
|
|
||||||
Send borgmatic logs to Loki. Defaults to false.
|
|
||||||
example: true
|
|
||||||
description: |
|
description: |
|
||||||
Configuration for a monitoring integration with Grafana Loki. You
|
Configuration for a monitoring integration with Grafana Loki. You
|
||||||
can send the logs to a self-hosted instance or create an account at
|
can send the logs to a self-hosted instance or create an account at
|
||||||
@@ -3162,12 +2928,6 @@ properties:
|
|||||||
project monitor. Used along with the data source name URL to
|
project monitor. Used along with the data source name URL to
|
||||||
construct a cron URL.
|
construct a cron URL.
|
||||||
example: mymonitor
|
example: mymonitor
|
||||||
environment:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Sentry monitor environment used in the call to Sentry. If
|
|
||||||
not set, the Sentry default is used.
|
|
||||||
example: production
|
|
||||||
states:
|
states:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -3221,8 +2981,7 @@ properties:
|
|||||||
findmnt_command:
|
findmnt_command:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Deprecated and unused. Was the command to use instead of
|
Command to use instead of "findmnt".
|
||||||
"findmnt".
|
|
||||||
example: /usr/local/bin/findmnt
|
example: /usr/local/bin/findmnt
|
||||||
description: |
|
description: |
|
||||||
Configuration for integration with the Btrfs filesystem.
|
Configuration for integration with the Btrfs filesystem.
|
||||||
@@ -3312,32 +3071,16 @@ properties:
|
|||||||
description: |
|
description: |
|
||||||
Command to use instead of "keepassxc-cli".
|
Command to use instead of "keepassxc-cli".
|
||||||
example: /usr/local/bin/keepassxc-cli
|
example: /usr/local/bin/keepassxc-cli
|
||||||
secret_tool_command:
|
|
||||||
type: string
|
|
||||||
description: |
|
|
||||||
Command to use instead of "secret-tool".
|
|
||||||
example: /usr/local/bin/secret-tool
|
|
||||||
ask_for_password:
|
|
||||||
type: boolean
|
|
||||||
description: |
|
|
||||||
Whether keepassxc-cli should prompt the user for a password.
|
|
||||||
Disabling this is only really useful if you're unlocking
|
|
||||||
your KeePassXC database with a key file instead of a
|
|
||||||
password. Ignored when using KeePassXC's secret service
|
|
||||||
integration. Defaults to true.
|
|
||||||
example: false
|
|
||||||
key_file:
|
key_file:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
Path to a key file for unlocking the KeePassXC database.
|
Path to a key file for unlocking the KeePassXC database.
|
||||||
Ignored when using KeePassXC's secret service integration.
|
|
||||||
example: /path/to/keyfile
|
example: /path/to/keyfile
|
||||||
yubikey:
|
yubikey:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
YubiKey slot and optional serial number used to access the
|
YubiKey slot and optional serial number used to access the
|
||||||
KeePassXC database. Ignored when using KeePassXC's secret
|
KeePassXC database. The format is "<slot[:serial]>", where:
|
||||||
service integration. The format is "<slot[:serial]>", where:
|
|
||||||
* <slot> is the YubiKey slot number (e.g., `1` or `2`).
|
* <slot> is the YubiKey slot number (e.g., `1` or `2`).
|
||||||
* <serial> (optional) is the YubiKey's serial number (e.g.,
|
* <serial> (optional) is the YubiKey's serial number (e.g.,
|
||||||
`7370001`).
|
`7370001`).
|
||||||
|
|||||||
+204
-508
@@ -1,14 +1,9 @@
|
|||||||
import collections
|
import collections
|
||||||
import contextlib
|
|
||||||
import enum
|
import enum
|
||||||
import json
|
|
||||||
import locale
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
import textwrap
|
import textwrap
|
||||||
import time
|
|
||||||
|
|
||||||
import borgmatic.logger
|
import borgmatic.logger
|
||||||
|
|
||||||
@@ -19,9 +14,6 @@ ERROR_OUTPUT_MAX_LINE_COUNT = 25
|
|||||||
BORG_ERROR_EXIT_CODE_START = 2
|
BORG_ERROR_EXIT_CODE_START = 2
|
||||||
BORG_ERROR_EXIT_CODE_END = 99
|
BORG_ERROR_EXIT_CODE_END = 99
|
||||||
|
|
||||||
# See https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids
|
|
||||||
BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS = {101, 102, 104, 105, 106}
|
|
||||||
|
|
||||||
|
|
||||||
class Exit_status(enum.Enum):
|
class Exit_status(enum.Enum):
|
||||||
STILL_RUNNING = 1
|
STILL_RUNNING = 1
|
||||||
@@ -30,32 +22,7 @@ class Exit_status(enum.Enum):
|
|||||||
ERROR = 4
|
ERROR = 4
|
||||||
|
|
||||||
|
|
||||||
def command_is_borg(command, borg_local_path):
|
def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None):
|
||||||
'''
|
|
||||||
Given a command as a sequence and the Borg local path, return whether that command is a call to
|
|
||||||
Borg.
|
|
||||||
'''
|
|
||||||
parsed_command = command.split(' ', 1) if isinstance(command, str) else command
|
|
||||||
|
|
||||||
if not parsed_command:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return bool(borg_local_path and parsed_command[0] == borg_local_path)
|
|
||||||
|
|
||||||
|
|
||||||
BORG_EXIT_CODE_TO_DESCRIPTION = {
|
|
||||||
100: 'File changed while we backed it up',
|
|
||||||
101: 'Include pattern never matched',
|
|
||||||
102: 'General backup issue',
|
|
||||||
103: 'File type or inode changed while we backed it up',
|
|
||||||
104: 'Backup OS issue',
|
|
||||||
105: 'Backup permission issue',
|
|
||||||
106: 'Backup IO issue',
|
|
||||||
107: 'Backup file not found',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None): # noqa: PLR0911
|
|
||||||
'''
|
'''
|
||||||
Return an Exit_status value (e.g. SUCCESS, ERROR, or WARNING) based on interpreting the given
|
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. If a Borg local path is given and matches the process' command, then interpret the
|
||||||
@@ -68,47 +35,39 @@ def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_code
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
return Exit_status.SUCCESS
|
return Exit_status.SUCCESS
|
||||||
|
|
||||||
if not command_is_borg(command, borg_local_path):
|
if borg_local_path and command[0] == borg_local_path:
|
||||||
return Exit_status.ERROR
|
# 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')
|
||||||
|
|
||||||
description = BORG_EXIT_CODE_TO_DESCRIPTION.get(exit_code)
|
if treat_as == 'error':
|
||||||
description_parenthetical = f' ({description})' if description else ''
|
logger.error(
|
||||||
|
f'Treating exit code {exit_code} as an error, as per configuration',
|
||||||
|
)
|
||||||
|
return Exit_status.ERROR
|
||||||
|
|
||||||
# First try looking for the exit code in the borg_exit_codes configuration.
|
if treat_as == 'warning':
|
||||||
for entry in borg_exit_codes or ():
|
logger.warning(
|
||||||
if entry.get('code') == exit_code:
|
f'Treating exit code {exit_code} as a warning, as per configuration',
|
||||||
treat_as = entry.get('treat_as')
|
)
|
||||||
|
return Exit_status.WARNING
|
||||||
|
|
||||||
if treat_as == 'error':
|
# If the exit code doesn't have explicit configuration, then fall back to the default Borg
|
||||||
logger.error(
|
# behavior.
|
||||||
f'Treating exit code {exit_code}{description_parenthetical} as an error, as per configuration',
|
return (
|
||||||
|
Exit_status.ERROR
|
||||||
|
if (
|
||||||
|
exit_code < 0
|
||||||
|
or (
|
||||||
|
exit_code >= BORG_ERROR_EXIT_CODE_START
|
||||||
|
and exit_code <= BORG_ERROR_EXIT_CODE_END
|
||||||
)
|
)
|
||||||
return Exit_status.ERROR
|
)
|
||||||
|
else Exit_status.WARNING
|
||||||
if treat_as == 'warning':
|
|
||||||
logger.warning(
|
|
||||||
f'Treating exit code {exit_code}{description_parenthetical} 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
|
|
||||||
# behavior of treating Borg errors as errors and some Borg warnings as errors.
|
|
||||||
if exit_code in BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS:
|
|
||||||
logger.error(
|
|
||||||
f'Treating exit code {exit_code}{description_parenthetical} as an error, as per borgmatic defaults',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return Exit_status.ERROR
|
return Exit_status.ERROR
|
||||||
|
|
||||||
if exit_code < 0 or (
|
|
||||||
exit_code >= BORG_ERROR_EXIT_CODE_START and exit_code <= BORG_ERROR_EXIT_CODE_END
|
|
||||||
):
|
|
||||||
return Exit_status.ERROR
|
|
||||||
|
|
||||||
logger.warning(
|
|
||||||
f'Treating exit code {exit_code}{description_parenthetical} as a warning, as per borgmatic defaults',
|
|
||||||
)
|
|
||||||
return Exit_status.WARNING
|
|
||||||
|
|
||||||
|
|
||||||
def command_for_process(process):
|
def command_for_process(process):
|
||||||
@@ -119,402 +78,156 @@ def command_for_process(process):
|
|||||||
return process.args if isinstance(process.args, str) else ' '.join(process.args)
|
return process.args if isinstance(process.args, str) else ' '.join(process.args)
|
||||||
|
|
||||||
|
|
||||||
def output_buffers_for_process(process, exclude_stdouts):
|
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
|
Given a process as an instance of subprocess.Popen and a sequence of stdouts to exclude, return
|
||||||
the process stdout and stderr as a tuple—but exclude the stdout if it's in the given stdouts to
|
either the process's stdout or stderr. The idea is that if stdout is excluded for a process, we
|
||||||
exclude.
|
still have stderr to log.
|
||||||
'''
|
'''
|
||||||
return tuple(
|
return process.stderr if process.stdout in exclude_stdouts else process.stdout
|
||||||
buffer for buffer in (process.stdout, process.stderr) if buffer not in exclude_stdouts
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
BORG_LOG_LEVEL_ELEVATION_THRESHOLD = 10
|
def append_last_lines(last_lines, captured_output, line, output_log_level):
|
||||||
|
|
||||||
|
|
||||||
def borg_json_log_line_to_record(line, log_level):
|
|
||||||
'''
|
'''
|
||||||
Given a single Borg "--log-json"-style log line and a log level, return the line converted to a
|
Given a rolling list of last lines, a list of captured output, a line to append, and an output
|
||||||
logging.LogRecord instance. Return None if the line can't be parsed as JSON.
|
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.
|
||||||
If Borg provides a log level in its JSON, prefer logging at that level. But if Borg doesn't
|
|
||||||
provide a log level—or the log level given to this function is just a little bit higher than
|
|
||||||
Borg's—elevate to that level. This supports use cases like elevating Borg's INFO level logs to
|
|
||||||
borgmatic's custom ANSWER level so that requested data shows up even at the default verbosity.
|
|
||||||
'''
|
'''
|
||||||
with contextlib.suppress(json.JSONDecodeError, TypeError, KeyError, AttributeError):
|
last_lines.append(line)
|
||||||
log_data = json.loads(line)
|
|
||||||
log_type = log_data.get('type')
|
|
||||||
|
|
||||||
if log_type == 'log_message':
|
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||||
borg_log_level = logging._nameToLevel.get(log_data.get('levelname'))
|
last_lines.pop(0)
|
||||||
log_level_delta = 0 if log_level is None else log_level - borg_log_level
|
|
||||||
|
|
||||||
if log_level_delta > 0 and log_level_delta < BORG_LOG_LEVEL_ELEVATION_THRESHOLD:
|
if output_log_level is None:
|
||||||
return logging.makeLogRecord(
|
captured_output.append(line)
|
||||||
dict(
|
else:
|
||||||
levelno=log_level,
|
logger.log(output_log_level, line)
|
||||||
created=log_data.get('time'),
|
|
||||||
msg=log_data.get('message'),
|
|
||||||
levelname=logging.getLevelName(log_level),
|
|
||||||
name=log_data.get('name'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return logging.makeLogRecord(
|
|
||||||
dict(
|
|
||||||
levelno=borg_log_level,
|
|
||||||
created=log_data.get('time'),
|
|
||||||
msg=log_data.get('message'),
|
|
||||||
levelname=log_data.get('levelname'),
|
|
||||||
name=log_data.get('name'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if log_type == 'file_status':
|
|
||||||
return logging.makeLogRecord(
|
|
||||||
dict(
|
|
||||||
levelno=log_level,
|
|
||||||
created=time.time(),
|
|
||||||
msg=f'{log_data.get("status")} {log_data.get("path")}',
|
|
||||||
levelname=logging.getLevelName(log_level),
|
|
||||||
name='borg.file_status',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def log_line_to_record(line, log_level):
|
def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path, borg_exit_codes): # noqa: PLR0912
|
||||||
'''
|
'''
|
||||||
Given a log data dict for a single Borg log entry and a log level, return it converted to a
|
Given a sequence of subprocess.Popen() instances for multiple processes, log the output for each
|
||||||
logging.LogRecord instance.
|
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 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.
|
||||||
|
|
||||||
|
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.
|
||||||
'''
|
'''
|
||||||
return logging.makeLogRecord(
|
# Map from output buffer to sequence of last lines.
|
||||||
dict(
|
buffer_last_lines = collections.defaultdict(list)
|
||||||
msg=line,
|
process_for_output_buffer = {
|
||||||
levelno=log_level,
|
output_buffer_for_process(process, exclude_stdouts): process
|
||||||
levelname=logging.getLevelName(log_level),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_log_line(line, log_level, elevate_stderr, borg_local_path, command):
|
|
||||||
'''
|
|
||||||
Given a raw output line from an external program, whether this line came from stderr and should
|
|
||||||
be elevated to error/warning, the Borg local path, and the command as a sequence, return a
|
|
||||||
logging.LogRecord instance containing its parsed data.
|
|
||||||
|
|
||||||
If the command being run is Borg, and the log line is JSON-formatted log data, then grab the log
|
|
||||||
level from it and log the parsed JSON to be consumed later by a Python logging.Formatter.
|
|
||||||
|
|
||||||
But for non-Borg commands, elevate stderr-sourced logs to ERROR. The one exception is if the log
|
|
||||||
came from stderr and the string "warning:" appears at the start of the log line. In that case,
|
|
||||||
just elevate the log level to a WARN.
|
|
||||||
'''
|
|
||||||
if command_is_borg(command, borg_local_path):
|
|
||||||
log_record = borg_json_log_line_to_record(line, log_level)
|
|
||||||
|
|
||||||
if log_record:
|
|
||||||
return log_record
|
|
||||||
|
|
||||||
if elevate_stderr:
|
|
||||||
return log_line_to_record(
|
|
||||||
line, logging.WARNING if line.lower().startswith('warning:') else logging.ERROR
|
|
||||||
)
|
|
||||||
|
|
||||||
return log_line_to_record(line, log_level)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_log_record(log_record, last_lines=None):
|
|
||||||
'''
|
|
||||||
Given a log record to be logged and a rolling list of last lines, append the record's message to
|
|
||||||
the last lines (if given). Then (if the log level is not None), log the record.
|
|
||||||
|
|
||||||
Return the log record.
|
|
||||||
'''
|
|
||||||
log_message = log_record.getMessage()
|
|
||||||
|
|
||||||
if last_lines is not None:
|
|
||||||
last_lines.append(log_message)
|
|
||||||
|
|
||||||
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
|
|
||||||
last_lines.pop(0)
|
|
||||||
|
|
||||||
if log_record.levelno is not None:
|
|
||||||
logger.handle(log_record)
|
|
||||||
|
|
||||||
return log_record
|
|
||||||
|
|
||||||
|
|
||||||
READ_CHUNK_SIZE = 4096
|
|
||||||
|
|
||||||
|
|
||||||
def read_lines(buffer, process, line_separator='\n'):
|
|
||||||
'''
|
|
||||||
Given a Python buffer (like stdout) ready for reading, its process, and a line separator,
|
|
||||||
repeatedly yield a tuple of (decoded) lines from the buffer until the process has exited.
|
|
||||||
|
|
||||||
It is assumed that this function's generator is used in conjunction with an external select()
|
|
||||||
call to know when to read more lines. Otherwise, the generator will busywait if it's called in a
|
|
||||||
tight loop.
|
|
||||||
'''
|
|
||||||
data = b''
|
|
||||||
encoded_separator = line_separator.encode()
|
|
||||||
separator_size = len(encoded_separator)
|
|
||||||
encoding = locale.getpreferredencoding()
|
|
||||||
|
|
||||||
while True:
|
|
||||||
chunk = os.read(buffer.fileno(), READ_CHUNK_SIZE)
|
|
||||||
|
|
||||||
if not chunk: # EOF
|
|
||||||
# The process is still running, so we keep running too.
|
|
||||||
if process.poll() is None: # pragma: no cover
|
|
||||||
continue
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
data += chunk
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# Split the data into lines, holding back anything leftover that might
|
|
||||||
# be a partial line.
|
|
||||||
while True:
|
|
||||||
separator_position = data.find(encoded_separator)
|
|
||||||
|
|
||||||
if separator_position == -1:
|
|
||||||
break
|
|
||||||
|
|
||||||
lines.append(data[:separator_position].decode(encoding))
|
|
||||||
data = data[separator_position + separator_size :]
|
|
||||||
|
|
||||||
yield tuple(lines)
|
|
||||||
|
|
||||||
# Yield any leftover data from the end of the buffer.
|
|
||||||
if data:
|
|
||||||
yield (data.decode(encoding).rstrip(),)
|
|
||||||
|
|
||||||
|
|
||||||
Buffer_reader = collections.namedtuple(
|
|
||||||
'Buffer_reader',
|
|
||||||
('lines', 'process'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Process_metadata = collections.namedtuple(
|
|
||||||
'Process_metadata',
|
|
||||||
('last_lines', 'capture'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def log_buffer_lines(
|
|
||||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr=False
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
|
||||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, and
|
|
||||||
whether to capture stderr, read and log any ready output lines from the buffers. Additionally,
|
|
||||||
for any log records with a log level the same as the output log level, yield those log messages
|
|
||||||
for capture.
|
|
||||||
|
|
||||||
This function just does one "turn of the crank" of logging buffer output. It is intended to be
|
|
||||||
called repeatedly to continue to process buffers.
|
|
||||||
'''
|
|
||||||
if not buffer_readers:
|
|
||||||
return
|
|
||||||
|
|
||||||
(ready_buffers, _, _) = select.select(buffer_readers.keys(), [], [])
|
|
||||||
|
|
||||||
for ready_buffer in ready_buffers:
|
|
||||||
reader = buffer_readers[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 reader.process and reader.process.poll() is not None:
|
|
||||||
for other_process in process_metadatas:
|
|
||||||
if (
|
|
||||||
other_process.poll() is None
|
|
||||||
and other_process.stdout
|
|
||||||
and other_process.stdout not in buffer_readers
|
|
||||||
):
|
|
||||||
# Add the process's output to buffer_readers to ensure it'll get read.
|
|
||||||
buffer_readers[other_process.stdout] = Buffer_reader(
|
|
||||||
read_lines(other_process.stdout, other_process), other_process
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
lines = next(reader.lines)
|
|
||||||
except StopIteration:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if not line or not reader.process:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Keep the last few lines of output in case the process errors and we need the
|
|
||||||
# output for the exception below.
|
|
||||||
log_record = handle_log_record(
|
|
||||||
parse_log_line(
|
|
||||||
line=line,
|
|
||||||
log_level=output_log_level,
|
|
||||||
elevate_stderr=(ready_buffer == reader.process.stderr and not capture_stderr),
|
|
||||||
borg_local_path=borg_local_path,
|
|
||||||
command=reader.process.args,
|
|
||||||
),
|
|
||||||
last_lines=process_metadatas[reader.process].last_lines,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
log_record.levelno is None or log_record.levelno == output_log_level
|
|
||||||
) and process_metadatas[reader.process].capture:
|
|
||||||
yield log_record.getMessage()
|
|
||||||
|
|
||||||
|
|
||||||
def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path, borg_exit_codes):
|
|
||||||
'''
|
|
||||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
|
||||||
Process_metadata instance, Borg's local path, a sequence of exit code configuration dicts, check
|
|
||||||
the given processes for error or warning exit codes. If found, vent or kill any running
|
|
||||||
processes. In the case of an error exit code, raise. In the case of warning, return
|
|
||||||
Exit_status.WARNING. Otherwise, return None.
|
|
||||||
'''
|
|
||||||
result_status = None
|
|
||||||
|
|
||||||
for process in process_metadatas:
|
|
||||||
exit_code = process.poll() if buffer_readers else process.wait()
|
|
||||||
|
|
||||||
if exit_code is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
exit_status = interpret_exit_code(process.args, exit_code, borg_local_path, borg_exit_codes)
|
|
||||||
|
|
||||||
if exit_status not in {Exit_status.ERROR, Exit_status.WARNING}:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Something has gone wrong. So vent each process' output buffer to prevent it from
|
|
||||||
# hanging. And then kill the process.
|
|
||||||
for other_process in process_metadatas:
|
|
||||||
if other_process.poll() is None:
|
|
||||||
other_process.stdout.read(0)
|
|
||||||
other_process.kill()
|
|
||||||
|
|
||||||
if exit_status == Exit_status.WARNING:
|
|
||||||
result_status = Exit_status.WARNING
|
|
||||||
continue
|
|
||||||
|
|
||||||
last_lines = process_metadatas[process].last_lines
|
|
||||||
|
|
||||||
# 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, '...')
|
|
||||||
|
|
||||||
raise subprocess.CalledProcessError(
|
|
||||||
exit_code,
|
|
||||||
command_for_process(process),
|
|
||||||
'\n'.join(last_lines),
|
|
||||||
)
|
|
||||||
|
|
||||||
return result_status
|
|
||||||
|
|
||||||
|
|
||||||
def log_remaining_buffer_lines(
|
|
||||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr=False
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
|
||||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, and
|
|
||||||
whether to capture stderr, drain and log any remaining output lines from the buffers until
|
|
||||||
they're empty. Additionally, for any log records with a log level the same as the output log
|
|
||||||
level, yield those log messages for capture.
|
|
||||||
'''
|
|
||||||
for output_buffer, reader in buffer_readers.items():
|
|
||||||
if not reader.process:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for lines in reader.lines:
|
|
||||||
for line in lines:
|
|
||||||
log_record = handle_log_record(
|
|
||||||
parse_log_line(
|
|
||||||
line=line.rstrip(),
|
|
||||||
log_level=output_log_level,
|
|
||||||
elevate_stderr=(
|
|
||||||
output_buffer == reader.process.stderr and not capture_stderr
|
|
||||||
),
|
|
||||||
borg_local_path=borg_local_path,
|
|
||||||
command=reader.process.args,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
log_record.levelno is None or log_record.levelno == output_log_level
|
|
||||||
) and process_metadatas[reader.process].capture:
|
|
||||||
yield log_record.getMessage()
|
|
||||||
|
|
||||||
|
|
||||||
def log_outputs(
|
|
||||||
processes,
|
|
||||||
exclude_stdouts,
|
|
||||||
output_log_level,
|
|
||||||
borg_local_path,
|
|
||||||
borg_exit_codes,
|
|
||||||
capture_stderr=False,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Given a sequence of subprocess.Popen() instances for multiple processes, log the outputs (stderr
|
|
||||||
and stdout). Use the requested output log level for stdout, but always log stderr to the ERROR
|
|
||||||
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 the output log level is None, then instead of logging, capture the output for the last
|
|
||||||
process given and yield it one line at a time. This includes stderr if capture stderr is set.
|
|
||||||
But if the output log level is not None, don't yield anything.
|
|
||||||
|
|
||||||
This yielding means that this function is a generator, and must be consumed in order to execute.
|
|
||||||
|
|
||||||
Use the given Borg local path and exit code configuration to decide what's an error and what's a
|
|
||||||
warning. If any stdouts are given to exclude, then for any matching processes, ignore those
|
|
||||||
buffers. Also 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 Process_metadata instance. By convention, the last process is the
|
|
||||||
# process to capture.
|
|
||||||
process_metadatas = {
|
|
||||||
process: Process_metadata(last_lines=[], capture=bool(process == processes[-1]))
|
|
||||||
for process in processes
|
|
||||||
}
|
|
||||||
|
|
||||||
# Map from buffer to Buffer_reader instance.
|
|
||||||
buffer_readers = {
|
|
||||||
buffer: Buffer_reader(read_lines(buffer, process), process)
|
|
||||||
for process in processes
|
for process in processes
|
||||||
if process.stdout or process.stderr
|
if process.stdout or process.stderr
|
||||||
for buffer in output_buffers_for_process(process, exclude_stdouts)
|
|
||||||
}
|
}
|
||||||
|
output_buffers = list(process_for_output_buffer.keys())
|
||||||
|
captured_outputs = collections.defaultdict(list)
|
||||||
|
still_running = True
|
||||||
|
|
||||||
# Log output lines for each process until they all exit.
|
# Log output for each process until they all exit.
|
||||||
while True:
|
while True: # noqa: PLR1702
|
||||||
yield from log_buffer_lines(
|
if output_buffers:
|
||||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
|
(ready_buffers, _, _) = select.select(output_buffers, [], [])
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
for ready_buffer in ready_buffers:
|
||||||
raise_for_process_errors(
|
ready_process = process_for_output_buffer.get(ready_buffer)
|
||||||
buffer_readers, process_metadatas, borg_local_path, borg_exit_codes
|
|
||||||
)
|
# The "ready" process has exited, but it might be a pipe destination with other
|
||||||
== Exit_status.WARNING
|
# 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
|
break
|
||||||
|
|
||||||
if all(process.poll() is not None for process in processes):
|
still_running = False
|
||||||
break
|
|
||||||
|
|
||||||
# Now that all processes have exited, drain and consume any last output.
|
for process in processes:
|
||||||
yield from log_remaining_buffer_lines(
|
exit_code = process.poll() if output_buffers else process.wait()
|
||||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
|
|
||||||
)
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
SECRET_COMMAND_FLAG_NAMES = {'--password'}
|
SECRET_COMMAND_FLAG_NAMES = {'--password'}
|
||||||
@@ -587,16 +300,16 @@ def execute_command(
|
|||||||
close_fds=False, # Necessary for passing credentials via anonymous pipe.
|
close_fds=False, # Necessary for passing credentials via anonymous pipe.
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Execute the given command (a sequence of command/argument strings) and log its stdout output at
|
Execute the given command (a sequence of command/argument strings) and log its output at the
|
||||||
the given log level. If an open output file object is given, then write stdout to the file and
|
given log level. If an open output file object is given, then write stdout to the file and only
|
||||||
only log stderr. If an open input file object is given, then read stdin from the file. If shell
|
log stderr. If an open input file object is given, then read stdin from the file. If shell is
|
||||||
is True, execute the command within a shell. If an environment variables dict is given, then
|
True, execute the command within a shell. If an environment variables dict is given, then pass
|
||||||
pass it into the command. If a working directory is given, use that as the present working
|
it into the command. If a working directory is given, use that as the present working directory
|
||||||
directory when running the command. If a Borg local path is given, and the command matches it
|
when running the command. If a Borg local path is given, and the command matches it (regardless
|
||||||
(regardless of arguments), treat exit code 1 as a warning instead of an error. But if Borg exit
|
of arguments), treat exit code 1 as a warning instead of an error. But if Borg exit codes are
|
||||||
codes are given as a sequence of exit code configuration dicts, then use that configuration to
|
given as a sequence of exit code configuration dicts, then use that configuration to decide
|
||||||
decide what's an error and what's a warning. If run to completion is False, then return the
|
what's an error and what's a warning. If run to completion is False, then return the process for
|
||||||
process for the command without executing it to completion.
|
the command without executing it to completion.
|
||||||
|
|
||||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||||
'''
|
'''
|
||||||
@@ -608,7 +321,7 @@ def execute_command(
|
|||||||
command,
|
command,
|
||||||
stdin=input_file,
|
stdin=input_file,
|
||||||
stdout=None if do_not_capture else (output_file or subprocess.PIPE),
|
stdout=None if do_not_capture else (output_file or subprocess.PIPE),
|
||||||
stderr=None if do_not_capture else subprocess.PIPE,
|
stderr=None if do_not_capture else (subprocess.PIPE if output_file else subprocess.STDOUT),
|
||||||
shell=shell,
|
shell=shell,
|
||||||
env=environment,
|
env=environment,
|
||||||
cwd=working_directory,
|
cwd=working_directory,
|
||||||
@@ -618,14 +331,12 @@ def execute_command(
|
|||||||
return process
|
return process
|
||||||
|
|
||||||
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
||||||
tuple(
|
log_outputs(
|
||||||
log_outputs(
|
(process,),
|
||||||
(process,),
|
(input_file, output_file),
|
||||||
(input_file, output_file),
|
output_log_level,
|
||||||
output_log_level,
|
borg_local_path,
|
||||||
borg_local_path,
|
borg_exit_codes,
|
||||||
borg_exit_codes,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -633,7 +344,6 @@ def execute_command(
|
|||||||
|
|
||||||
def execute_command_and_capture_output(
|
def execute_command_and_capture_output(
|
||||||
full_command,
|
full_command,
|
||||||
output_log_level=None,
|
|
||||||
input_file=None,
|
input_file=None,
|
||||||
capture_stderr=False,
|
capture_stderr=False,
|
||||||
shell=False,
|
shell=False,
|
||||||
@@ -645,18 +355,14 @@ def execute_command_and_capture_output(
|
|||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Execute the given command (a sequence of command/argument strings), capturing and returning its
|
Execute the given command (a sequence of command/argument strings), capturing and returning its
|
||||||
output (stdout) as a generator that yields one line at a time. The generator must be consumed in
|
output (stdout). If an input file descriptor is given, then pipe it to the command's stdin. If
|
||||||
order for the called command to execute.
|
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 environment variables dict is given, then pass it into
|
||||||
If an output log level is given, then instead of suppressing log output, also output the
|
the command. If a working directory is given, use that as the present working directory when
|
||||||
captured lines at the given log level. If an input file descriptor is given, then pipe it to
|
running the command. If a Borg local path is given, and the command matches it (regardless of
|
||||||
the command's stdin. If capture stderr is True, then capture stderr in addition to stdout. If
|
arguments), treat exit code 1 as a warning instead of an error. But if Borg exit codes are given
|
||||||
shell is True, execute the command within a shell. If an environment variables dict is given,
|
as a sequence of exit code configuration dicts, then use that configuration to decide what's an
|
||||||
then pass it into the command. If a working directory is given, use that as the present working
|
error and what's a warning.
|
||||||
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.
|
|
||||||
|
|
||||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||||
'''
|
'''
|
||||||
@@ -664,13 +370,10 @@ def execute_command_and_capture_output(
|
|||||||
command = ' '.join(full_command) if shell else full_command
|
command = ' '.join(full_command) if shell else full_command
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen( # noqa: S603
|
output = subprocess.check_output( # noqa: S603
|
||||||
command,
|
command,
|
||||||
stdin=input_file,
|
stdin=input_file,
|
||||||
stdout=subprocess.PIPE,
|
stderr=subprocess.STDOUT if capture_stderr else None,
|
||||||
stderr=subprocess.PIPE
|
|
||||||
if capture_stderr or command_is_borg(command, borg_local_path)
|
|
||||||
else None,
|
|
||||||
shell=shell,
|
shell=shell,
|
||||||
env=environment,
|
env=environment,
|
||||||
cwd=working_directory,
|
cwd=working_directory,
|
||||||
@@ -683,20 +386,9 @@ def execute_command_and_capture_output(
|
|||||||
):
|
):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if error.output is not None:
|
output = error.output
|
||||||
yield from iter(error.output.decode(locale.getpreferredencoding()).splitlines())
|
|
||||||
|
|
||||||
return
|
return output.decode() if output is not None else None
|
||||||
|
|
||||||
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
|
||||||
yield from log_outputs(
|
|
||||||
(process,),
|
|
||||||
(input_file,),
|
|
||||||
output_log_level,
|
|
||||||
borg_local_path,
|
|
||||||
borg_exit_codes,
|
|
||||||
capture_stderr=capture_stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def execute_command_with_processes(
|
def execute_command_with_processes(
|
||||||
@@ -713,23 +405,20 @@ def execute_command_with_processes(
|
|||||||
close_fds=False, # Necessary for passing credentials via anonymous pipe.
|
close_fds=False, # Necessary for passing credentials via anonymous pipe.
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Execute the given command (a sequence of command/argument strings) and log its stdout output at
|
Execute the given command (a sequence of command/argument strings) and log its output at the
|
||||||
the given log level. Simultaneously, continue to poll one or more active processes so that they
|
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
|
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.
|
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 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)
|
if output log level is None, instead suppress logging and return the captured output for (only)
|
||||||
the given command as a generator that yields one line at a time. The generator must be consumed
|
the given command. If an open input file object is given, then read stdin from the file. If
|
||||||
in order for the called command to execute—regardless of the output log level.
|
shell is True, execute the command within a shell. If an environment variables dict is given,
|
||||||
|
then pass it into the command. If a working directory is given, use that as the present working
|
||||||
If an open input file object is given, then read stdin from the file. If shell is True, execute
|
directory when running the command. If a Borg local path is given, then for any matching command
|
||||||
the command within a shell. If an environment variables dict is given, then pass it into the
|
or process (regardless of arguments), treat exit code 1 as a warning instead of an error. But if
|
||||||
command. If a working directory is given, use that as the present working directory when running
|
Borg exit codes are given as a sequence of exit code configuration dicts, then use that
|
||||||
the command. If a Borg local path is given, then for any matching command or process (regardless
|
configuration to decide what's an error and what's a warning.
|
||||||
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
|
Raise subprocesses.CalledProcessError if an error occurs while running the command or in the
|
||||||
upstream process.
|
upstream process.
|
||||||
@@ -743,7 +432,9 @@ def execute_command_with_processes(
|
|||||||
command,
|
command,
|
||||||
stdin=input_file,
|
stdin=input_file,
|
||||||
stdout=None if do_not_capture else (output_file or subprocess.PIPE),
|
stdout=None if do_not_capture else (output_file or subprocess.PIPE),
|
||||||
stderr=None if do_not_capture else subprocess.PIPE,
|
stderr=(
|
||||||
|
None if do_not_capture else (subprocess.PIPE if output_file else subprocess.STDOUT)
|
||||||
|
),
|
||||||
shell=shell,
|
shell=shell,
|
||||||
env=environment,
|
env=environment,
|
||||||
cwd=working_directory,
|
cwd=working_directory,
|
||||||
@@ -760,10 +451,15 @@ def execute_command_with_processes(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
||||||
yield from log_outputs(
|
captured_outputs = log_outputs(
|
||||||
(*processes, command_process),
|
(*processes, command_process),
|
||||||
(input_file, output_file),
|
(input_file, output_file),
|
||||||
output_log_level,
|
output_log_level,
|
||||||
borg_local_path,
|
borg_local_path,
|
||||||
borg_exit_codes,
|
borg_exit_codes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if output_log_level is None:
|
||||||
|
return captured_outputs.get(command_process)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|||||||
+41
-64
@@ -13,71 +13,39 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
SOFT_FAIL_EXIT_CODE = 75
|
SOFT_FAIL_EXIT_CODE = 75
|
||||||
BORG_PLACEHOLDER_NAMES = {
|
BORG_PLACEHOLDERS = {
|
||||||
'hostname',
|
'{hostname}',
|
||||||
'fqdn',
|
'{fqdn}',
|
||||||
'reverse-fqdn',
|
'{reverse-fqdn}',
|
||||||
'now',
|
'{now}',
|
||||||
'utcnow',
|
'{utcnow}',
|
||||||
'unixtime',
|
'{unixtime}',
|
||||||
'user',
|
'{user}',
|
||||||
'pid',
|
'{pid}',
|
||||||
'borgversion',
|
'{borgversion}',
|
||||||
'borgmajor',
|
'{borgmajor}',
|
||||||
'borgminor',
|
'{borgminor}',
|
||||||
'borgpatch',
|
'{borgpatch}',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
VARIABLE_PATTERN = re.compile(r'(?P<left_escape>\\)?\{(?P<name>[\w]+)(?P<right_escape>\\)?\}')
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_variable(match, context, hook_description):
|
|
||||||
'''
|
|
||||||
Given a re.Match instance of VARIABLE_PATTERN representing a matched variable name to be
|
|
||||||
interpolated, a context dict, and a description of the current command hook, lookup the matched
|
|
||||||
variable name within the given context and return its value.
|
|
||||||
|
|
||||||
If the match is escaped with backslashes, then instead of resolving the variable's value, strip
|
|
||||||
off the backslashing and return the literal value.
|
|
||||||
|
|
||||||
If the variable name isn't found in the given context (and isn't a Borg placeholder), then
|
|
||||||
warn and return the literal value.
|
|
||||||
'''
|
|
||||||
name = match.group('name')
|
|
||||||
|
|
||||||
# The would-be variable is escaped, so strip off the escaping and return the result without
|
|
||||||
# resolving the name.
|
|
||||||
if match.group('left_escape') and match.group('right_escape'):
|
|
||||||
return '{' + name + '}'
|
|
||||||
|
|
||||||
value = context.get(name)
|
|
||||||
|
|
||||||
# The matched variable is in the context, so return its value.
|
|
||||||
if value is not None:
|
|
||||||
return shlex.quote(str(value))
|
|
||||||
|
|
||||||
# The matched variable name isn't in the context. Warn about variables unknown to borgmatic, but
|
|
||||||
# don't warn if the variable name happens to be a Borg placeholder, as Borg should hopefully
|
|
||||||
# consume it.
|
|
||||||
if name not in BORG_PLACEHOLDER_NAMES:
|
|
||||||
logger.warning(
|
|
||||||
f'Variable "{name}" is not supported in the {hook_description} hook',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Return the whole string unaltered.
|
|
||||||
return match.group(0)
|
|
||||||
|
|
||||||
|
|
||||||
def interpolate_context(hook_description, command, context):
|
def interpolate_context(hook_description, command, context):
|
||||||
'''
|
'''
|
||||||
Given a config filename, a hook description, a single hook command, and a dict of context
|
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.
|
names/values, interpolate the values by "{name}" into the command and return the result.
|
||||||
'''
|
'''
|
||||||
return VARIABLE_PATTERN.sub(
|
for name, value in context.items():
|
||||||
functools.partial(resolve_variable, context=context, hook_description=hook_description),
|
command = command.replace(f'{{{name}}}', shlex.quote(str(value)))
|
||||||
command,
|
|
||||||
)
|
for unsupported_variable in re.findall(r'\{\w+\}', command):
|
||||||
|
# Warn about variables unknown to borgmatic, but don't warn if the variable name happens to
|
||||||
|
# be a Borg placeholder, as Borg should hopefully consume it.
|
||||||
|
if unsupported_variable not in BORG_PLACEHOLDERS:
|
||||||
|
logger.warning(
|
||||||
|
f'Variable "{unsupported_variable}" is not supported in the {hook_description} hook',
|
||||||
|
)
|
||||||
|
|
||||||
|
return command
|
||||||
|
|
||||||
|
|
||||||
def make_environment(current_environment, sys_module=sys):
|
def make_environment(current_environment, sys_module=sys):
|
||||||
@@ -96,22 +64,27 @@ def make_environment(current_environment, sys_module=sys):
|
|||||||
return environment
|
return environment
|
||||||
|
|
||||||
|
|
||||||
def filter_hooks(command_hooks, before=None, after=None, action_names=None, state_names=None):
|
def filter_hooks(command_hooks, before=None, after=None, action_names=None, step_names=None, state_names=None):
|
||||||
'''
|
'''
|
||||||
Given a sequence of command hook dicts from configuration and one or more filters (before name,
|
Given a sequence of command hook dicts from configuration and one or more filters (before name,
|
||||||
after name, a sequence of action names, and/or a sequence of execution result state names),
|
after name, a sequence of action names, a sequence of sub-action steps, and/or a sequence of
|
||||||
filter down the command hooks to just the ones that match the given filters.
|
execution result state names), filter down the command hooks to just the ones that match the
|
||||||
|
given filters.
|
||||||
'''
|
'''
|
||||||
return tuple(
|
return tuple(
|
||||||
hook_config
|
hook_config
|
||||||
for hook_config in command_hooks or ()
|
for hook_config in command_hooks or ()
|
||||||
for config_action_names in (hook_config.get('when'),)
|
for config_action_names in (hook_config.get('when'),)
|
||||||
|
for config_step_names in (hook_config.get('steps'),)
|
||||||
for config_state_names in (hook_config.get('states'),)
|
for config_state_names in (hook_config.get('states'),)
|
||||||
if before is None or hook_config.get('before') == before
|
if before is None or hook_config.get('before') == before
|
||||||
if after is None or hook_config.get('after') == after
|
if after is None or hook_config.get('after') == after
|
||||||
if action_names is None
|
if action_names is None
|
||||||
or config_action_names is None
|
or config_action_names is None
|
||||||
or set(config_action_names or ()).intersection(set(action_names))
|
or set(config_action_names or ()).intersection(set(action_names))
|
||||||
|
if step_names is None
|
||||||
|
or config_step_names is None
|
||||||
|
or set(config_step_names or ()).intersection(set(step_names))
|
||||||
if state_names is None
|
if state_names is None
|
||||||
or config_state_names is None
|
or config_state_names is None
|
||||||
or set(config_state_names or ()).intersection(set(state_names))
|
or set(config_state_names or ()).intersection(set(state_names))
|
||||||
@@ -179,7 +152,6 @@ def execute_hooks(command_hooks, umask, working_directory, dry_run, **context):
|
|||||||
shell=True,
|
shell=True,
|
||||||
environment=make_environment(os.environ),
|
environment=make_environment(os.environ),
|
||||||
working_directory=working_directory,
|
working_directory=working_directory,
|
||||||
close_fds=True,
|
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if original_umask:
|
if original_umask:
|
||||||
@@ -197,7 +169,8 @@ class Before_after_hooks:
|
|||||||
before_after='do_stuff',
|
before_after='do_stuff',
|
||||||
umask=config.get('umask'),
|
umask=config.get('umask'),
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
action_names=['create'],
|
action_names=['check'],
|
||||||
|
step_names=['spot'],
|
||||||
):
|
):
|
||||||
do()
|
do()
|
||||||
some()
|
some()
|
||||||
@@ -215,13 +188,14 @@ class Before_after_hooks:
|
|||||||
working_directory,
|
working_directory,
|
||||||
dry_run,
|
dry_run,
|
||||||
action_names=None,
|
action_names=None,
|
||||||
|
step_names=None,
|
||||||
**context,
|
**context,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Given a sequence of command hook configuration dicts, the before/after name, a umask to run
|
Given a sequence of command hook configuration dicts, the before/after name, a umask to run
|
||||||
commands with, a working directory to run commands with, a dry run flag, a sequence of
|
commands with, a working directory to run commands with, a dry run flag, a sequence of
|
||||||
action names, and any context for the executed commands, save those data points for use
|
action names, a sequence of sub-action step names, and any context for the executed
|
||||||
below.
|
commands, save those data points for use below.
|
||||||
'''
|
'''
|
||||||
self.command_hooks = command_hooks
|
self.command_hooks = command_hooks
|
||||||
self.before_after = before_after
|
self.before_after = before_after
|
||||||
@@ -229,6 +203,7 @@ class Before_after_hooks:
|
|||||||
self.working_directory = working_directory
|
self.working_directory = working_directory
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
self.action_names = action_names
|
self.action_names = action_names
|
||||||
|
self.step_names = step_names
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
@@ -241,6 +216,7 @@ class Before_after_hooks:
|
|||||||
self.command_hooks,
|
self.command_hooks,
|
||||||
before=self.before_after,
|
before=self.before_after,
|
||||||
action_names=self.action_names,
|
action_names=self.action_names,
|
||||||
|
step_names=self.step_names,
|
||||||
),
|
),
|
||||||
self.umask,
|
self.umask,
|
||||||
self.working_directory,
|
self.working_directory,
|
||||||
@@ -267,6 +243,7 @@ class Before_after_hooks:
|
|||||||
self.command_hooks,
|
self.command_hooks,
|
||||||
after=self.before_after,
|
after=self.before_after,
|
||||||
action_names=self.action_names,
|
action_names=self.action_names,
|
||||||
|
step_names=self.step_names,
|
||||||
state_names=['fail' if exception_type else 'finish'],
|
state_names=['fail' if exception_type else 'finish'],
|
||||||
),
|
),
|
||||||
self.umask,
|
self.umask,
|
||||||
|
|||||||
@@ -7,34 +7,19 @@ import borgmatic.execute
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
SECRET_SERVICE_DATABASE_PATH = 'secret-service'
|
|
||||||
|
|
||||||
|
|
||||||
def load_credential(hook_config, config, credential_parameters):
|
def load_credential(hook_config, config, credential_parameters):
|
||||||
'''
|
'''
|
||||||
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
|
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
|
||||||
containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch
|
containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch
|
||||||
the corresponding KeePassXC credential and return it. Or use secret-tool if the database path
|
the corresponding KeePassXC credential and return it.
|
||||||
is "secret-service", indicating that KeePassXC's secret service integration should be used
|
|
||||||
instead.
|
|
||||||
|
|
||||||
Raise ValueError if keepassxc-cli or secret-tool can't retrieve the credential.
|
Raise ValueError if keepassxc-cli can't retrieve the credential.
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
(database_path, attribute_name) = credential_parameters
|
(database_path, attribute_name) = credential_parameters
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(f'Invalid KeePassXC credential parameters: {credential_parameters}')
|
raise ValueError(f'Invalid KeePassXC credential parameters: {credential_parameters}')
|
||||||
|
|
||||||
if database_path == SECRET_SERVICE_DATABASE_PATH:
|
|
||||||
command = (
|
|
||||||
*shlex.split((hook_config or {}).get('secret_tool_command', 'secret-tool')),
|
|
||||||
*('lookup', 'Path', attribute_name),
|
|
||||||
)
|
|
||||||
|
|
||||||
return '\n'.join(borgmatic.execute.execute_command_and_capture_output(command)).rstrip(
|
|
||||||
os.linesep
|
|
||||||
)
|
|
||||||
|
|
||||||
expanded_database_path = os.path.expanduser(database_path)
|
expanded_database_path = os.path.expanduser(database_path)
|
||||||
|
|
||||||
if not os.path.exists(expanded_database_path):
|
if not os.path.exists(expanded_database_path):
|
||||||
@@ -44,7 +29,6 @@ def load_credential(hook_config, config, credential_parameters):
|
|||||||
command = (
|
command = (
|
||||||
tuple(shlex.split((hook_config or {}).get('keepassxc_cli_command', 'keepassxc-cli')))
|
tuple(shlex.split((hook_config or {}).get('keepassxc_cli_command', 'keepassxc-cli')))
|
||||||
+ ('show', '--show-protected', '--attributes', 'Password')
|
+ ('show', '--show-protected', '--attributes', 'Password')
|
||||||
+ (('--no-password',) if not (hook_config or {}).get('ask_for_password', True) else ())
|
|
||||||
+ (
|
+ (
|
||||||
('--key-file', hook_config['key_file'])
|
('--key-file', hook_config['key_file'])
|
||||||
if hook_config and hook_config.get('key_file')
|
if hook_config and hook_config.get('key_file')
|
||||||
@@ -58,6 +42,4 @@ def load_credential(hook_config, config, credential_parameters):
|
|||||||
+ (expanded_database_path, attribute_name) # Ensure database and entry are last.
|
+ (expanded_database_path, attribute_name) # Ensure database and entry are last.
|
||||||
)
|
)
|
||||||
|
|
||||||
return '\n'.join(borgmatic.execute.execute_command_and_capture_output(command)).rstrip(
|
return borgmatic.execute.execute_command_and_capture_output(command).rstrip(os.linesep)
|
||||||
os.linesep
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ def load_credential(hook_config, config, credential_parameters):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return '\n'.join(borgmatic.execute.execute_command_and_capture_output(command)).rstrip(
|
return borgmatic.execute.execute_command_and_capture_output(command).rstrip(os.linesep)
|
||||||
os.linesep
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(
|
with open(
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import glob
|
import glob
|
||||||
import importlib
|
import importlib
|
||||||
import itertools
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,32 +18,6 @@ def use_streaming(hook_config, config): # pragma: no cover
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW = 10
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_config_path_symlinks(path):
|
|
||||||
'''
|
|
||||||
Given a path, resolve and yield each successive symlink until the final non-symlink target. If
|
|
||||||
the given path isn't a symlink, then just yield it.
|
|
||||||
|
|
||||||
The purpose of this is to ensure that configuration files that are behind a symbolic link (or
|
|
||||||
several) actually get backed up.
|
|
||||||
|
|
||||||
Raise ValueError if we have to follow too many symlinks without getting to the final target.
|
|
||||||
'''
|
|
||||||
original_path = os.path.normpath(path)
|
|
||||||
|
|
||||||
for _ in range(MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW):
|
|
||||||
yield path
|
|
||||||
|
|
||||||
if not os.path.islink(path):
|
|
||||||
return
|
|
||||||
|
|
||||||
path = os.path.normpath(os.path.join(os.path.dirname(path), os.readlink(path)))
|
|
||||||
|
|
||||||
raise ValueError(f'Too many symlinks to follow for configuration path: {original_path}')
|
|
||||||
|
|
||||||
|
|
||||||
def dump_data_sources(
|
def dump_data_sources(
|
||||||
hook_config,
|
hook_config,
|
||||||
config,
|
config,
|
||||||
@@ -61,9 +33,6 @@ def dump_data_sources(
|
|||||||
the archive. But skip this if the bootstrap store_config_files option is False or if this is a
|
the archive. But skip this if the bootstrap store_config_files option is False or if this is a
|
||||||
dry run.
|
dry run.
|
||||||
|
|
||||||
If any configuration paths are symlinks, then store each symlink along with any destination
|
|
||||||
paths as well.
|
|
||||||
|
|
||||||
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
Return an empty sequence, since there are no ongoing dump processes from this hook.
|
||||||
'''
|
'''
|
||||||
if hook_config and hook_config.get('store_config_files') is False:
|
if hook_config and hook_config.get('store_config_files') is False:
|
||||||
@@ -75,10 +44,6 @@ def dump_data_sources(
|
|||||||
'manifest.json',
|
'manifest.json',
|
||||||
)
|
)
|
||||||
|
|
||||||
resolved_config_paths = tuple(
|
|
||||||
itertools.chain.from_iterable(resolve_config_path_symlinks(path) for path in config_paths)
|
|
||||||
)
|
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -88,36 +53,33 @@ def dump_data_sources(
|
|||||||
json.dump(
|
json.dump(
|
||||||
{
|
{
|
||||||
'borgmatic_version': importlib.metadata.version('borgmatic'),
|
'borgmatic_version': importlib.metadata.version('borgmatic'),
|
||||||
'config_paths': resolved_config_paths,
|
'config_paths': config_paths,
|
||||||
},
|
},
|
||||||
manifest_file,
|
manifest_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
patterns.extend(
|
||||||
patterns,
|
borgmatic.borg.pattern.Pattern(
|
||||||
|
config_path,
|
||||||
|
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
||||||
|
)
|
||||||
|
for config_path in config_paths
|
||||||
|
)
|
||||||
|
patterns.append(
|
||||||
borgmatic.borg.pattern.Pattern(
|
borgmatic.borg.pattern.Pattern(
|
||||||
os.path.join(borgmatic_runtime_directory, 'bootstrap'),
|
os.path.join(borgmatic_runtime_directory, 'bootstrap'),
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
for config_path in resolved_config_paths:
|
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
|
||||||
patterns,
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
|
||||||
config_path,
|
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, patterns, dry_run):
|
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, dry_run):
|
||||||
'''
|
'''
|
||||||
Given a bootstrap configuration dict, a configuration dict, the borgmatic runtime directory, the
|
Given a bootstrap configuration dict, a configuration dict, the borgmatic runtime directory, and
|
||||||
configured patterns, and whether this is a dry run, then remove the manifest file created above.
|
whether this is a dry run, then remove the manifest file created above. If this is a dry run,
|
||||||
If this is a dry run, then don't actually remove anything.
|
then don't actually remove anything.
|
||||||
'''
|
'''
|
||||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||||
|
|
||||||
@@ -150,10 +112,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Restores are implemented via the separate, purpose-specific "bootstrap" action rather than the
|
Restores are implemented via the separate, purpose-specific "bootstrap" action rather than the
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import collections
|
import collections
|
||||||
import functools
|
|
||||||
import glob
|
import glob
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.execute
|
import borgmatic.execute
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
import borgmatic.hooks.data_source.snapshot
|
import borgmatic.hooks.data_source.snapshot
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -23,49 +22,105 @@ def use_streaming(hook_config, config): # pragma: no cover
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
BTRFS_SUBVOLUME_INODE_NUMBER = 256
|
def get_contained_subvolume_paths(btrfs_command, subvolume_path):
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
|
||||||
def path_is_a_subvolume(path):
|
|
||||||
'''
|
'''
|
||||||
Given a path, return whether it is a Btrfs subvolume. Return False if the path doesn't exist.
|
Given the path of a Btrfs subvolume, return it in a sequence along with the paths of its
|
||||||
|
contained subvolumes.
|
||||||
|
|
||||||
As a performance optimization, multiple calls to this function with the same path are cached.
|
If the btrfs command errors, log that error and return an empty sequence.
|
||||||
'''
|
'''
|
||||||
if path == os.environ.get('BTRFS_TEST_SUBVOLUME_PATH'): # pragma: no cover
|
try:
|
||||||
return True
|
btrfs_output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
|
(
|
||||||
|
*btrfs_command.split(' '),
|
||||||
|
'subvolume',
|
||||||
|
'list',
|
||||||
|
subvolume_path,
|
||||||
|
),
|
||||||
|
close_fds=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
logger.debug(
|
||||||
|
f'Ignoring Btrfs subvolume {subvolume_path} because of error listing its subvolumes: {error}',
|
||||||
|
)
|
||||||
|
|
||||||
|
return ()
|
||||||
|
|
||||||
|
return (
|
||||||
|
subvolume_path,
|
||||||
|
*tuple(
|
||||||
|
os.path.join(subvolume_path, line.split(' ')[-1])
|
||||||
|
for line in btrfs_output.splitlines()
|
||||||
|
if line.strip()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FINDMNT_BTRFS_ROOT_SUBVOLUME_OPTION = 'subvolid=5'
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_subvolume_paths(btrfs_command, findmnt_command):
|
||||||
|
'''
|
||||||
|
Given btrfs and findmnt commands to run, get the sorted paths for all Btrfs subvolumes on the
|
||||||
|
system.
|
||||||
|
'''
|
||||||
|
findmnt_output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
|
(
|
||||||
|
*findmnt_command.split(' '),
|
||||||
|
'-t', # Filesystem type.
|
||||||
|
'btrfs',
|
||||||
|
'--json',
|
||||||
|
'--list', # Request a flat list instead of a nested subvolume hierarchy.
|
||||||
|
),
|
||||||
|
close_fds=True,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return os.stat(path).st_ino == BTRFS_SUBVOLUME_INODE_NUMBER
|
return tuple(
|
||||||
except FileNotFoundError:
|
sorted(
|
||||||
return False
|
itertools.chain.from_iterable(
|
||||||
|
# If findmnt gave us a Btrfs root filesystem, list the subvolumes within it.
|
||||||
|
# This is necessary because findmnt only returns a subvolume's mount point
|
||||||
|
# rather than its original subvolume path (which can differ). For instance,
|
||||||
|
# a subvolume might exist at /mnt/subvolume but be mounted at /home/myuser.
|
||||||
|
# findmnt is still useful though because it's a global way to discover all
|
||||||
|
# Btrfs subvolumes—even if we have to do some additional legwork ourselves.
|
||||||
|
(
|
||||||
|
get_contained_subvolume_paths(btrfs_command, filesystem['target'])
|
||||||
|
if FINDMNT_BTRFS_ROOT_SUBVOLUME_OPTION in filesystem['options'].split(',')
|
||||||
|
else (filesystem['target'],)
|
||||||
|
)
|
||||||
|
for filesystem in json.loads(findmnt_output)['filesystems']
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError as error:
|
||||||
|
raise ValueError(f'Invalid {findmnt_command} JSON output: {error}')
|
||||||
|
except KeyError as error:
|
||||||
|
raise ValueError(f'Invalid {findmnt_command} output: Missing key "{error}"')
|
||||||
|
|
||||||
|
|
||||||
|
Subvolume = collections.namedtuple('Subvolume', ('path', 'contained_patterns'), defaults=((),))
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
|
||||||
def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
||||||
'''
|
'''
|
||||||
Given a btrfs command, a subvolume path, and a property name to lookup, return the value of the
|
Given a btrfs command, a subvolume path, and a property name to lookup, return the value of the
|
||||||
corresponding property.
|
corresponding property.
|
||||||
|
|
||||||
Raise subprocess.CalledProcessError if the btrfs command errors.
|
Raise subprocess.CalledProcessError if the btrfs command errors.
|
||||||
|
|
||||||
As a performance optimization, multiple calls to this function with the same arguments are
|
|
||||||
cached.
|
|
||||||
'''
|
'''
|
||||||
output = '\n'.join(
|
output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
borgmatic.execute.execute_command_and_capture_output(
|
(
|
||||||
(
|
*btrfs_command.split(' '),
|
||||||
*btrfs_command.split(' '),
|
'property',
|
||||||
'property',
|
'get',
|
||||||
'get',
|
'-t', # Type.
|
||||||
'-t', # Type.
|
'subvol',
|
||||||
'subvol',
|
subvolume_path,
|
||||||
subvolume_path,
|
property_name,
|
||||||
property_name,
|
),
|
||||||
),
|
close_fds=True,
|
||||||
close_fds=True,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -79,71 +134,37 @@ def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
|||||||
}.get(value, value)
|
}.get(value, value)
|
||||||
|
|
||||||
|
|
||||||
def get_containing_subvolume_path(btrfs_command, path):
|
def omit_read_only_subvolume_paths(btrfs_command, subvolume_paths):
|
||||||
'''
|
'''
|
||||||
Given a btrfs command and a path, return the subvolume path that contains the given path (or is
|
Given a Btrfs command to run and a sequence of Btrfs subvolume paths, filter them down to just
|
||||||
the same as the path).
|
those that are read-write. The idea is that Btrfs can't actually snapshot a read-only subvolume,
|
||||||
|
so we should just ignore them.
|
||||||
If there is no such subvolume path or the containing subvolume is read-only, return None.
|
|
||||||
'''
|
'''
|
||||||
# Probe the given pattern's path and all of its parents, grandparents, etc. to try to find a
|
retained_subvolume_paths = []
|
||||||
# Btrfs subvolume.
|
|
||||||
for candidate_path in (
|
|
||||||
path,
|
|
||||||
*tuple(str(ancestor) for ancestor in pathlib.PurePath(path).parents),
|
|
||||||
):
|
|
||||||
if not path_is_a_subvolume(candidate_path):
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
for subvolume_path in subvolume_paths:
|
||||||
try:
|
try:
|
||||||
if get_subvolume_property(btrfs_command, candidate_path, 'ro'):
|
if get_subvolume_property(btrfs_command, subvolume_path, 'ro'):
|
||||||
logger.debug(f'Ignoring Btrfs subvolume {candidate_path} because it is read-only')
|
logger.debug(f'Ignoring Btrfs subvolume {subvolume_path} because it is read-only')
|
||||||
|
else:
|
||||||
return None
|
retained_subvolume_paths.append(subvolume_path)
|
||||||
|
except subprocess.CalledProcessError as error: # noqa: PERF203
|
||||||
logger.debug(f'Path {candidate_path} is a Btrfs subvolume')
|
|
||||||
|
|
||||||
return candidate_path
|
|
||||||
except subprocess.CalledProcessError as error:
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'Error determining read-only status of Btrfs subvolume {candidate_path}: {error}',
|
f'Error determining read-only status of Btrfs subvolume {subvolume_path}: {error}',
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return tuple(retained_subvolume_paths)
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_subvolume_paths(btrfs_command, patterns):
|
def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
|
||||||
'''
|
|
||||||
Given a btrfs command and a sequence of patterns, get the sorted paths for all Btrfs subvolumes
|
|
||||||
containing those patterns.
|
|
||||||
'''
|
|
||||||
return tuple(
|
|
||||||
sorted(
|
|
||||||
{
|
|
||||||
subvolume_path
|
|
||||||
for pattern in patterns
|
|
||||||
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
|
|
||||||
if pattern.source == borgmatic.borg.pattern.Pattern_source.CONFIG
|
|
||||||
for subvolume_path in (get_containing_subvolume_path(btrfs_command, pattern.path),)
|
|
||||||
if subvolume_path
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Subvolume = collections.namedtuple('Subvolume', ('path', 'contained_patterns'), defaults=((),))
|
|
||||||
|
|
||||||
|
|
||||||
def get_subvolumes(btrfs_command, patterns):
|
|
||||||
'''
|
'''
|
||||||
Given a Btrfs command to run and a sequence of configured patterns, find the intersection
|
Given a Btrfs command to run and a sequence of configured patterns, find the intersection
|
||||||
between the current Btrfs filesystem/subvolume paths and the paths of any patterns. The idea is
|
between the current Btrfs filesystem and subvolume paths and the paths of any patterns. The
|
||||||
that these pattern paths represent the requested subvolumes to snapshot.
|
idea is that these pattern paths represent the requested subvolumes to snapshot.
|
||||||
|
|
||||||
Only include subvolumes that contain at least one root pattern sourced from borgmatic
|
Only include subvolumes that contain at least one root pattern sourced from borgmatic
|
||||||
configuration (as opposed to generated elsewhere in borgmatic).
|
configuration (as opposed to generated elsewhere in borgmatic). But if patterns is None, then
|
||||||
|
return all subvolumes instead, sorted by path.
|
||||||
|
|
||||||
Return the result as a sequence of matching Subvolume instances.
|
Return the result as a sequence of matching Subvolume instances.
|
||||||
'''
|
'''
|
||||||
@@ -151,10 +172,15 @@ def get_subvolumes(btrfs_command, patterns):
|
|||||||
subvolumes = []
|
subvolumes = []
|
||||||
|
|
||||||
# For each subvolume path, match it against the given patterns to find the subvolumes to
|
# For each subvolume path, match it against the given patterns to find the subvolumes to
|
||||||
# backup. Sort the subvolumes from longest to shortest mount points, so longer subvolumes get
|
# backup. Sort the subvolumes from longest to shortest mount points, so longer mount points get
|
||||||
# a whack at the candidate pattern piñata before their parents do. (Patterns are consumed during
|
# a whack at the candidate pattern piñata before their parents do. (Patterns are consumed during
|
||||||
# this process, so no two subvolumes end up with the same contained patterns.)
|
# this process, so no two subvolumes end up with the same contained patterns.)
|
||||||
for subvolume_path in reversed(get_all_subvolume_paths(btrfs_command, patterns)):
|
for subvolume_path in reversed(
|
||||||
|
omit_read_only_subvolume_paths(
|
||||||
|
btrfs_command,
|
||||||
|
get_all_subvolume_paths(btrfs_command, findmnt_command),
|
||||||
|
),
|
||||||
|
):
|
||||||
subvolumes.extend(
|
subvolumes.extend(
|
||||||
Subvolume(subvolume_path, contained_patterns)
|
Subvolume(subvolume_path, contained_patterns)
|
||||||
for contained_patterns in (
|
for contained_patterns in (
|
||||||
@@ -163,7 +189,8 @@ def get_subvolumes(btrfs_command, patterns):
|
|||||||
candidate_patterns,
|
candidate_patterns,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if any(
|
if patterns is None
|
||||||
|
or any(
|
||||||
pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
|
pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
|
||||||
and pattern.source == borgmatic.borg.pattern.Pattern_source.CONFIG
|
and pattern.source == borgmatic.borg.pattern.Pattern_source.CONFIG
|
||||||
for pattern in contained_patterns
|
for pattern in contained_patterns
|
||||||
@@ -173,7 +200,7 @@ def get_subvolumes(btrfs_command, patterns):
|
|||||||
return tuple(sorted(subvolumes, key=lambda subvolume: subvolume.path))
|
return tuple(sorted(subvolumes, key=lambda subvolume: subvolume.path))
|
||||||
|
|
||||||
|
|
||||||
BORGMATIC_SNAPSHOT_PREFIX = '.borgmatic-snapshot'
|
BORGMATIC_SNAPSHOT_PREFIX = '.borgmatic-snapshot-'
|
||||||
|
|
||||||
|
|
||||||
def make_snapshot_path(subvolume_path):
|
def make_snapshot_path(subvolume_path):
|
||||||
@@ -182,7 +209,7 @@ def make_snapshot_path(subvolume_path):
|
|||||||
'''
|
'''
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
subvolume_path,
|
subvolume_path,
|
||||||
f'{BORGMATIC_SNAPSHOT_PREFIX}',
|
f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}',
|
||||||
# Included so that the snapshot ends up in the Borg archive at the "original" subvolume path.
|
# Included so that the snapshot ends up in the Borg archive at the "original" subvolume path.
|
||||||
) + subvolume_path.rstrip(os.path.sep)
|
) + subvolume_path.rstrip(os.path.sep)
|
||||||
|
|
||||||
@@ -195,16 +222,16 @@ def make_snapshot_exclude_pattern(subvolume_path): # pragma: no cover
|
|||||||
directory within the snapshot itself. For instance, if you have a Btrfs subvolume at /mnt and
|
directory within the snapshot itself. For instance, if you have a Btrfs subvolume at /mnt and
|
||||||
make a snapshot of it at:
|
make a snapshot of it at:
|
||||||
|
|
||||||
/mnt/.borgmatic-snapshot/mnt
|
/mnt/.borgmatic-snapshot-1234/mnt
|
||||||
|
|
||||||
... then the snapshot itself will have an empty directory at:
|
... then the snapshot itself will have an empty directory at:
|
||||||
|
|
||||||
/mnt/.borgmatic-snapshot/mnt/.borgmatic-snapshot
|
/mnt/.borgmatic-snapshot-1234/mnt/.borgmatic-snapshot-1234
|
||||||
|
|
||||||
So to prevent that from ending up in the Borg archive, this function produces an exclude pattern
|
So to prevent that from ending up in the Borg archive, this function produces an exclude pattern
|
||||||
to exclude that path.
|
to exclude that path.
|
||||||
'''
|
'''
|
||||||
snapshot_directory = f'{BORGMATIC_SNAPSHOT_PREFIX}'
|
snapshot_directory = f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}'
|
||||||
|
|
||||||
return borgmatic.borg.pattern.Pattern(
|
return borgmatic.borg.pattern.Pattern(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
@@ -237,7 +264,7 @@ def make_borg_snapshot_pattern(subvolume_path, pattern):
|
|||||||
|
|
||||||
rewritten_path = initial_caret + os.path.join(
|
rewritten_path = initial_caret + os.path.join(
|
||||||
subvolume_path,
|
subvolume_path,
|
||||||
f'{BORGMATIC_SNAPSHOT_PREFIX}',
|
f'{BORGMATIC_SNAPSHOT_PREFIX}{os.getpid()}',
|
||||||
# Use the Borg 1.4+ "slashdot" hack to prevent the snapshot path prefix from getting
|
# Use the Borg 1.4+ "slashdot" hack to prevent the snapshot path prefix from getting
|
||||||
# included in the archive—but only if there's not already a slashdot hack present in the
|
# included in the archive—but only if there's not already a slashdot hack present in the
|
||||||
# pattern.
|
# pattern.
|
||||||
@@ -267,6 +294,7 @@ def snapshot_subvolume(btrfs_command, subvolume_path, snapshot_path): # pragma:
|
|||||||
*btrfs_command.split(' '),
|
*btrfs_command.split(' '),
|
||||||
'subvolume',
|
'subvolume',
|
||||||
'snapshot',
|
'snapshot',
|
||||||
|
'-r', # Read-only.
|
||||||
subvolume_path,
|
subvolume_path,
|
||||||
snapshot_path,
|
snapshot_path,
|
||||||
),
|
),
|
||||||
@@ -297,15 +325,11 @@ def dump_data_sources(
|
|||||||
dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually snapshotting anything)' if dry_run else ''
|
||||||
logger.info(f'Snapshotting Btrfs subvolumes{dry_run_label}')
|
logger.info(f'Snapshotting Btrfs subvolumes{dry_run_label}')
|
||||||
|
|
||||||
if 'findmnt_command' in hook_config:
|
|
||||||
logger.warning(
|
|
||||||
'The Btrfs "findmnt_command" option is deprecated and will be removed from a future release; findmnt is no longer used',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Based on the configured patterns, determine Btrfs subvolumes to backup. Only consider those
|
# Based on the configured patterns, determine Btrfs subvolumes to backup. Only consider those
|
||||||
# patterns that came from actual user configuration (as opposed to, say, other hooks).
|
# patterns that came from actual user configuration (as opposed to, say, other hooks).
|
||||||
btrfs_command = hook_config.get('btrfs_command', 'btrfs')
|
btrfs_command = hook_config.get('btrfs_command', 'btrfs')
|
||||||
subvolumes = get_subvolumes(btrfs_command, patterns)
|
findmnt_command = hook_config.get('findmnt_command', 'findmnt')
|
||||||
|
subvolumes = get_subvolumes(btrfs_command, findmnt_command, patterns)
|
||||||
|
|
||||||
if not subvolumes:
|
if not subvolumes:
|
||||||
logger.warning(f'No Btrfs subvolumes found to snapshot{dry_run_label}')
|
logger.warning(f'No Btrfs subvolumes found to snapshot{dry_run_label}')
|
||||||
@@ -323,11 +347,14 @@ def dump_data_sources(
|
|||||||
|
|
||||||
for pattern in subvolume.contained_patterns:
|
for pattern in subvolume.contained_patterns:
|
||||||
snapshot_pattern = make_borg_snapshot_pattern(subvolume.path, pattern)
|
snapshot_pattern = make_borg_snapshot_pattern(subvolume.path, pattern)
|
||||||
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
|
|
||||||
|
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
# Attempt to update the pattern in place, since pattern order matters to Borg.
|
||||||
patterns, make_snapshot_exclude_pattern(subvolume.path)
|
try:
|
||||||
)
|
patterns[patterns.index(pattern)] = snapshot_pattern
|
||||||
|
except ValueError:
|
||||||
|
patterns.append(snapshot_pattern)
|
||||||
|
|
||||||
|
patterns.append(make_snapshot_exclude_pattern(subvolume.path))
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -348,12 +375,11 @@ def delete_snapshot(btrfs_command, snapshot_path): # pragma: no cover
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, patterns, dry_run):
|
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, dry_run):
|
||||||
'''
|
'''
|
||||||
Given a Btrfs configuration dict, a configuration dict, the borgmatic runtime directory, the
|
Given a Btrfs configuration dict, a configuration dict, the borgmatic runtime directory, and
|
||||||
configured patterns, and whether this is a dry run, delete any Btrfs snapshots created by
|
whether this is a dry run, delete any Btrfs snapshots created by borgmatic. If this is a dry run
|
||||||
borgmatic. If this is a dry run or Btrfs isn't configured in borgmatic's configuration, then
|
or Btrfs isn't configured in borgmatic's configuration, then don't actually remove anything.
|
||||||
don't actually remove anything.
|
|
||||||
'''
|
'''
|
||||||
if hook_config is None:
|
if hook_config is None:
|
||||||
return
|
return
|
||||||
@@ -361,9 +387,10 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
|||||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||||
|
|
||||||
btrfs_command = hook_config.get('btrfs_command', 'btrfs')
|
btrfs_command = hook_config.get('btrfs_command', 'btrfs')
|
||||||
|
findmnt_command = hook_config.get('findmnt_command', 'findmnt')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
all_subvolumes = get_subvolumes(btrfs_command, patterns)
|
all_subvolumes = get_subvolumes(btrfs_command, findmnt_command)
|
||||||
except FileNotFoundError as error:
|
except FileNotFoundError as error:
|
||||||
logger.debug(f'Could not find "{error.filename}" command')
|
logger.debug(f'Could not find "{error.filename}" command')
|
||||||
return
|
return
|
||||||
@@ -414,10 +441,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import logging
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import borgmatic.borg.pattern
|
|
||||||
from borgmatic.execute import execute_command_and_capture_output
|
from borgmatic.execute import execute_command_and_capture_output
|
||||||
|
|
||||||
IS_A_HOOK = False
|
IS_A_HOOK = False
|
||||||
@@ -72,15 +71,13 @@ def get_ip_from_container(container):
|
|||||||
last_error = None
|
last_error = None
|
||||||
for engine in engines:
|
for engine in engines:
|
||||||
try:
|
try:
|
||||||
output = '\n'.join(
|
output = execute_command_and_capture_output(
|
||||||
execute_command_and_capture_output(
|
(
|
||||||
(
|
engine,
|
||||||
engine,
|
'container',
|
||||||
'container',
|
'inspect',
|
||||||
'inspect',
|
'--format={{json .NetworkSettings}}',
|
||||||
'--format={{json .NetworkSettings}}',
|
container,
|
||||||
container,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
@@ -105,51 +102,3 @@ def get_ip_from_container(container):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Could not determine ip address for container '{container}'; running in host mode or userspace networking?"
|
f"Could not determine ip address for container '{container}'; running in host mode or userspace networking?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def inject_pattern(patterns, data_source_pattern, override_excludes=True):
|
|
||||||
'''
|
|
||||||
Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
|
|
||||||
insert the given data source pattern at the start of the list. The idea is that borgmatic is
|
|
||||||
injecting its own custom pattern specific to a data source hook into the user's configured
|
|
||||||
patterns so that the hook's data gets included in the backup.
|
|
||||||
|
|
||||||
As part of this injection, if the data source pattern is a root pattern and override_excludes is
|
|
||||||
True, also insert an "include" version of the given root pattern, in an attempt to preempt any
|
|
||||||
of the user's configured exclude patterns that may follow. The is to support use cases like
|
|
||||||
borgmatic injecting its own patterns for things like database dumps or bootstrap metadata, where
|
|
||||||
we don't want them to get accidentally excluded.
|
|
||||||
'''
|
|
||||||
if data_source_pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT and override_excludes:
|
|
||||||
patterns.insert(
|
|
||||||
0,
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
|
||||||
path=data_source_pattern.path,
|
|
||||||
type=borgmatic.borg.pattern.Pattern_type.INCLUDE,
|
|
||||||
style=data_source_pattern.style,
|
|
||||||
device=data_source_pattern.device,
|
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
patterns.insert(0, data_source_pattern)
|
|
||||||
|
|
||||||
|
|
||||||
def replace_pattern(patterns, pattern_to_replace, data_source_pattern):
|
|
||||||
'''
|
|
||||||
Given a list of borgmatic.borg.pattern.Pattern instances representing the configured patterns,
|
|
||||||
replace the given pattern with the given data source pattern. The idea is that borgmatic is
|
|
||||||
replacing a configured pattern with its own modified pattern specific to a data source hook so
|
|
||||||
that the hook's data gets included in the backup.
|
|
||||||
|
|
||||||
If the pattern to replace can't be found in the given patterns, then just inject the data source
|
|
||||||
pattern at the start of the list.
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
index = patterns.index(pattern_to_replace)
|
|
||||||
except ValueError:
|
|
||||||
inject_pattern(patterns, data_source_pattern, override_excludes=False)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
patterns[index] = data_source_pattern
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import fnmatch
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import borgmatic.actions.restore
|
import borgmatic.actions.restore
|
||||||
@@ -115,33 +114,16 @@ def remove_data_source_dumps(dump_path, data_source_type_name, dry_run):
|
|||||||
shutil.rmtree(dump_path)
|
shutil.rmtree(dump_path)
|
||||||
|
|
||||||
|
|
||||||
END_OF_STRING_PATTTERN = re.compile(r'\\z', flags=re.IGNORECASE)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_glob_patterns_to_borg_pattern(patterns):
|
def convert_glob_patterns_to_borg_pattern(patterns):
|
||||||
'''
|
'''
|
||||||
Convert a sequence of shell glob patterns like "/etc/*", "/tmp/*" to the corresponding Borg
|
Convert a sequence of shell glob patterns like "/etc/*", "/tmp/*" to the corresponding Borg
|
||||||
regular expression archive pattern as a single string like "re:etc/.*|tmp/.*".
|
regular expression archive pattern as a single string like "re:etc/.*|tmp/.*".
|
||||||
'''
|
'''
|
||||||
# This deserves some explanation. Here's what this code is doing for each shell glob pattern:
|
# Remove the "\z" or "\Z" generated by fnmatch.translate() because we don't want the pattern to
|
||||||
#
|
# match only at the end of a path, as directory format dumps require extracting files with paths
|
||||||
# * Strip off the leading slash, since Borg doesn't store leading slashes in archives.
|
# longer than the pattern. E.g., a pattern of "borgmatic/*/foo_databases/test" should also match
|
||||||
# * Use fnmatch.translate() to convert the pattern to a Borg pattern.
|
# paths like "borgmatic/*/foo_databases/test/toc.dat"
|
||||||
# * Remove the "\z" or \"Z" end-of-string special character generated by fnmatch.translate(),
|
|
||||||
# because Borg doesn't like it. Replace it with a "$" end-of-string special character
|
|
||||||
# instead. And yes, this is using a regular expression to modify a regular expression.
|
|
||||||
# * Do the above for each of:
|
|
||||||
# * The plain pattern. This supports the use case of a standard database where its name is a
|
|
||||||
# filename in the path. Example: borgmatic/foo_databases/hostname/test
|
|
||||||
# * The pattern with "/*" tacked onto the end of it. This supports the use case of a
|
|
||||||
# directory-format database where its name is a directory name in the path. Example:
|
|
||||||
# borgmatic/foo_databases/hostname/test/toc.dat
|
|
||||||
#
|
|
||||||
# Join the resulting transformed patterns together with "|" and return them as a string.
|
|
||||||
return 're:' + '|'.join(
|
return 're:' + '|'.join(
|
||||||
re.sub(END_OF_STRING_PATTTERN, '$', fnmatch.translate(stripped))
|
fnmatch.translate(pattern.lstrip('/')).replace('\\z', '').replace('\\Z', '')
|
||||||
+ '|'
|
|
||||||
+ re.sub(END_OF_STRING_PATTTERN, '$', fnmatch.translate(stripped + '/*'))
|
|
||||||
for pattern in patterns
|
for pattern in patterns
|
||||||
for stripped in (pattern.lstrip('/'),)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import subprocess
|
|||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.execute
|
import borgmatic.execute
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
import borgmatic.hooks.data_source.snapshot
|
import borgmatic.hooks.data_source.snapshot
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -44,19 +43,17 @@ def get_logical_volumes(lsblk_command, patterns=None):
|
|||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
devices_info = json.loads(
|
devices_info = json.loads(
|
||||||
'\n'.join(
|
borgmatic.execute.execute_command_and_capture_output(
|
||||||
borgmatic.execute.execute_command_and_capture_output(
|
# Use lsblk instead of lvs here because lvs can't show active mounts.
|
||||||
# Use lsblk instead of lvs here because lvs can't show active mounts.
|
(
|
||||||
(
|
*lsblk_command.split(' '),
|
||||||
*lsblk_command.split(' '),
|
'--output',
|
||||||
'--output',
|
'name,path,mountpoint,type',
|
||||||
'name,path,mountpoint,type',
|
'--json',
|
||||||
'--json',
|
'--list',
|
||||||
'--list',
|
|
||||||
),
|
|
||||||
close_fds=True,
|
|
||||||
),
|
),
|
||||||
)
|
close_fds=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError as error:
|
except json.JSONDecodeError as error:
|
||||||
raise ValueError(f'Invalid {lsblk_command} JSON output: {error}')
|
raise ValueError(f'Invalid {lsblk_command} JSON output: {error}')
|
||||||
@@ -278,7 +275,11 @@ def dump_data_sources(
|
|||||||
normalized_runtime_directory,
|
normalized_runtime_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
|
# Attempt to update the pattern in place, since pattern order matters to Borg.
|
||||||
|
try:
|
||||||
|
patterns[patterns.index(pattern)] = snapshot_pattern
|
||||||
|
except ValueError:
|
||||||
|
patterns.append(snapshot_pattern)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -323,21 +324,19 @@ def get_snapshots(lvs_command, snapshot_name=None):
|
|||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
snapshot_info = json.loads(
|
snapshot_info = json.loads(
|
||||||
'\n'.join(
|
borgmatic.execute.execute_command_and_capture_output(
|
||||||
borgmatic.execute.execute_command_and_capture_output(
|
# Use lvs instead of lsblk here because lsblk can't filter to just snapshots.
|
||||||
# Use lvs instead of lsblk here because lsblk can't filter to just snapshots.
|
(
|
||||||
(
|
*lvs_command.split(' '),
|
||||||
*lvs_command.split(' '),
|
'--report-format',
|
||||||
'--report-format',
|
'json',
|
||||||
'json',
|
'--options',
|
||||||
'--options',
|
'lv_name,lv_path',
|
||||||
'lv_name,lv_path',
|
'--select',
|
||||||
'--select',
|
'lv_attr =~ ^s', # Filter to just snapshots.
|
||||||
'lv_attr =~ ^s', # Filter to just snapshots.
|
|
||||||
),
|
|
||||||
close_fds=True,
|
|
||||||
),
|
),
|
||||||
)
|
close_fds=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
except json.JSONDecodeError as error:
|
except json.JSONDecodeError as error:
|
||||||
raise ValueError(f'Invalid {lvs_command} JSON output: {error}')
|
raise ValueError(f'Invalid {lvs_command} JSON output: {error}')
|
||||||
@@ -354,12 +353,12 @@ def get_snapshots(lvs_command, snapshot_name=None):
|
|||||||
raise ValueError(f'Invalid {lvs_command} output: Missing key "{error}"')
|
raise ValueError(f'Invalid {lvs_command} output: Missing key "{error}"')
|
||||||
|
|
||||||
|
|
||||||
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, patterns, dry_run): # noqa: PLR0912
|
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, dry_run): # noqa: PLR0912
|
||||||
'''
|
'''
|
||||||
Given an LVM configuration dict, a configuration dict, the borgmatic runtime directory, the
|
Given an LVM configuration dict, a configuration dict, the borgmatic runtime directory, and
|
||||||
configured patterns, and whether this is a dry run, unmount and delete any LVM snapshots created
|
whether this is a dry run, unmount and delete any LVM snapshots created by borgmatic. If this is
|
||||||
by borgmatic. If this is a dry run or LVM isn't configured in borgmatic's configuration, then
|
a dry run or LVM isn't configured in borgmatic's configuration, then don't actually remove
|
||||||
don't actually remove anything.
|
anything.
|
||||||
'''
|
'''
|
||||||
if hook_config is None:
|
if hook_config is None:
|
||||||
return
|
return
|
||||||
@@ -456,10 +455,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -6,7 +7,6 @@ import shlex
|
|||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.hooks.credential.parse
|
import borgmatic.hooks.credential.parse
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
from borgmatic.execute import (
|
from borgmatic.execute import (
|
||||||
execute_command,
|
execute_command,
|
||||||
execute_command_and_capture_output,
|
execute_command_and_capture_output,
|
||||||
@@ -66,9 +66,7 @@ def make_defaults_file_options(username=None, password=None, defaults_extra_file
|
|||||||
Do not use the returned value for multiple different command invocations. That will not work
|
Do not use the returned value for multiple different command invocations. That will not work
|
||||||
because each pipe is "used up" once read.
|
because each pipe is "used up" once read.
|
||||||
'''
|
'''
|
||||||
escaped_password = (
|
escaped_password = None if password is None else password.replace('\\', '\\\\')
|
||||||
None if password is None else password.replace('\\', '\\\\').replace('"', '\\"')
|
|
||||||
)
|
|
||||||
|
|
||||||
values = '\n'.join(
|
values = '\n'.join(
|
||||||
(
|
(
|
||||||
@@ -107,10 +105,6 @@ def make_defaults_file_options(username=None, password=None, defaults_extra_file
|
|||||||
return (f'--defaults-extra-file=/dev/fd/{read_file_descriptor}',)
|
return (f'--defaults-extra-file=/dev/fd/{read_file_descriptor}',)
|
||||||
|
|
||||||
|
|
||||||
EXCLUDED_SYSTEM_DATABASE_NAMES = ('information_schema', 'performance_schema', 'sys')
|
|
||||||
SYSTEM_DATABASE_NAME = 'mysql'
|
|
||||||
|
|
||||||
|
|
||||||
def database_names_to_dump(database, config, username, password, environment, dry_run):
|
def database_names_to_dump(database, config, username, password, environment, dry_run):
|
||||||
'''
|
'''
|
||||||
Given a requested database config, a configuration dict, a database username and password, an
|
Given a requested database config, a configuration dict, a database username and password, an
|
||||||
@@ -118,14 +112,7 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
names to dump. In the case of "all", query for the names of databases on the configured host and
|
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.
|
return them, excluding any system databases that will cause problems during restore.
|
||||||
'''
|
'''
|
||||||
skip_names = database.get('skip_names')
|
|
||||||
|
|
||||||
if database['name'] != 'all':
|
if database['name'] != 'all':
|
||||||
if skip_names:
|
|
||||||
logger.warning(
|
|
||||||
f'For MariaDB database {database["name"]}, ignoring the "skip_names" option, which is only supported for database "all"'
|
|
||||||
)
|
|
||||||
|
|
||||||
return (database['name'],)
|
return (database['name'],)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
@@ -137,8 +124,6 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
extra_options, defaults_extra_filename = parse_extra_options(database.get('list_options'))
|
extra_options, defaults_extra_filename = parse_extra_options(database.get('list_options'))
|
||||||
password_transport = database.get('password_transport', 'pipe')
|
password_transport = database.get('password_transport', 'pipe')
|
||||||
hostname = database_config.resolve_database_option('hostname', database)
|
hostname = database_config.resolve_database_option('hostname', database)
|
||||||
socket_path = database.get('socket_path')
|
|
||||||
|
|
||||||
show_command = (
|
show_command = (
|
||||||
mariadb_show_command
|
mariadb_show_command
|
||||||
+ (
|
+ (
|
||||||
@@ -150,7 +135,6 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
+ (('--host', hostname) if hostname else ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
||||||
+ (('--socket', socket_path) if socket_path else ())
|
|
||||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||||
+ (('--ssl',) if database.get('tls') is True else ())
|
+ (('--ssl',) if database.get('tls') is True else ())
|
||||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||||
@@ -160,27 +144,18 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
|
|
||||||
logger.debug('Querying for "all" MariaDB databases to dump')
|
logger.debug('Querying for "all" MariaDB databases to dump')
|
||||||
|
|
||||||
if skip_names:
|
show_output = execute_command_and_capture_output(show_command, environment=environment)
|
||||||
logger.debug(f'Skipping database names: {", ".join(skip_names)}')
|
|
||||||
|
|
||||||
show_lines = execute_command_and_capture_output(
|
|
||||||
show_command,
|
|
||||||
environment=environment,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dumping system databases directly doesn't work; too much gets dumped (including virtual tables
|
|
||||||
# that MariaDB recreates on startup) and so the dump isn't restorable. Therefore several system
|
|
||||||
# databases are excluded here. But also see below where select system tables from the "mysql"
|
|
||||||
# system table are dumped via the "--system" flag.
|
|
||||||
return tuple(
|
return tuple(
|
||||||
show_name.strip()
|
show_name
|
||||||
for show_name in show_lines
|
for show_name in show_output.strip().splitlines()
|
||||||
if show_name not in EXCLUDED_SYSTEM_DATABASE_NAMES
|
if show_name not in SYSTEM_DATABASE_NAMES
|
||||||
if not skip_names or show_name not in skip_names
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||||||
|
|
||||||
|
|
||||||
def execute_dump_command(
|
def execute_dump_command(
|
||||||
database,
|
database,
|
||||||
config,
|
config,
|
||||||
@@ -222,8 +197,6 @@ def execute_dump_command(
|
|||||||
extra_options, defaults_extra_filename = parse_extra_options(database.get('options'))
|
extra_options, defaults_extra_filename = parse_extra_options(database.get('options'))
|
||||||
password_transport = database.get('password_transport', 'pipe')
|
password_transport = database.get('password_transport', 'pipe')
|
||||||
hostname = database_config.resolve_database_option('hostname', database)
|
hostname = database_config.resolve_database_option('hostname', database)
|
||||||
socket_path = database.get('socket_path')
|
|
||||||
|
|
||||||
dump_command = (
|
dump_command = (
|
||||||
mariadb_dump_command
|
mariadb_dump_command
|
||||||
+ (
|
+ (
|
||||||
@@ -233,17 +206,14 @@ def execute_dump_command(
|
|||||||
)
|
)
|
||||||
+ extra_options
|
+ extra_options
|
||||||
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
|
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
|
||||||
+ ('--single-transaction',)
|
|
||||||
+ (('--host', hostname) if hostname else ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
||||||
+ (('--socket', socket_path) if socket_path else ())
|
|
||||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||||
+ (('--ssl',) if database.get('tls') is True else ())
|
+ (('--ssl',) if database.get('tls') is True else ())
|
||||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||||
+ ('--databases', '--events', '--routines', '--all-tablespaces')
|
+ ('--databases',)
|
||||||
+ (('--system=users,udfs,servers',) if SYSTEM_DATABASE_NAME in database_names else ())
|
+ database_names
|
||||||
+ tuple(name for name in database_names if name != SYSTEM_DATABASE_NAME)
|
|
||||||
+ ('--result-file', dump_filename)
|
+ ('--result-file', dump_filename)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -257,7 +227,6 @@ def execute_dump_command(
|
|||||||
dump_command,
|
dump_command,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
run_to_completion=False,
|
run_to_completion=False,
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -330,7 +299,6 @@ def dump_data_sources(
|
|||||||
|
|
||||||
raise ValueError('Cannot find any MariaDB databases to dump.')
|
raise ValueError('Cannot find any MariaDB databases to dump.')
|
||||||
|
|
||||||
# Database dumps to individual files.
|
|
||||||
if database['name'] == 'all' and database.get('format'):
|
if database['name'] == 'all' and database.get('format'):
|
||||||
for database_name in dump_database_names:
|
for database_name in dump_database_names:
|
||||||
dumps_metadata.append(
|
dumps_metadata.append(
|
||||||
@@ -343,7 +311,8 @@ def dump_data_sources(
|
|||||||
database.get('container'),
|
database.get('container'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
renamed_database = dict(database, name=database_name)
|
renamed_database = copy.copy(database)
|
||||||
|
renamed_database['name'] = database_name
|
||||||
processes.append(
|
processes.append(
|
||||||
execute_dump_command(
|
execute_dump_command(
|
||||||
renamed_database,
|
renamed_database,
|
||||||
@@ -357,7 +326,6 @@ def dump_data_sources(
|
|||||||
dry_run_label,
|
dry_run_label,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# Database dumps all to one file.
|
|
||||||
else:
|
else:
|
||||||
dumps_metadata.append(
|
dumps_metadata.append(
|
||||||
borgmatic.actions.restore.Dump(
|
borgmatic.actions.restore.Dump(
|
||||||
@@ -387,8 +355,7 @@ def dump_data_sources(
|
|||||||
dump.write_data_source_dumps_metadata(
|
dump.write_data_source_dumps_metadata(
|
||||||
borgmatic_runtime_directory, 'mariadb_databases', dumps_metadata
|
borgmatic_runtime_directory, 'mariadb_databases', dumps_metadata
|
||||||
)
|
)
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
patterns.append(
|
||||||
patterns,
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
borgmatic.borg.pattern.Pattern(
|
||||||
os.path.join(borgmatic_runtime_directory, 'mariadb_databases'),
|
os.path.join(borgmatic_runtime_directory, 'mariadb_databases'),
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
||||||
@@ -402,7 +369,6 @@ def remove_data_source_dumps(
|
|||||||
databases,
|
databases,
|
||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
patterns,
|
|
||||||
dry_run,
|
dry_run,
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
@@ -418,10 +384,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||||
@@ -431,24 +393,16 @@ def make_data_source_dump_patterns(
|
|||||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
|
||||||
),
|
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_runtime_directory),
|
make_dump_path(borgmatic_runtime_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_source_directory),
|
make_dump_path(borgmatic_source_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -475,7 +429,6 @@ def restore_data_source_dump(
|
|||||||
port = database_config.resolve_database_option(
|
port = database_config.resolve_database_option(
|
||||||
'port', data_source, connection_params, restore=True
|
'port', data_source, connection_params, restore=True
|
||||||
)
|
)
|
||||||
socket_path = database_config.resolve_database_option('socket_path', data_source, restore=True)
|
|
||||||
tls = database_config.resolve_database_option('tls', data_source, restore=True)
|
tls = database_config.resolve_database_option('tls', data_source, restore=True)
|
||||||
username = borgmatic.hooks.credential.parse.resolve_credential(
|
username = borgmatic.hooks.credential.parse.resolve_credential(
|
||||||
database_config.resolve_database_option(
|
database_config.resolve_database_option(
|
||||||
@@ -495,7 +448,6 @@ def restore_data_source_dump(
|
|||||||
)
|
)
|
||||||
extra_options, defaults_extra_filename = parse_extra_options(data_source.get('restore_options'))
|
extra_options, defaults_extra_filename = parse_extra_options(data_source.get('restore_options'))
|
||||||
password_transport = data_source.get('password_transport', 'pipe')
|
password_transport = data_source.get('password_transport', 'pipe')
|
||||||
|
|
||||||
restore_command = (
|
restore_command = (
|
||||||
mariadb_restore_command
|
mariadb_restore_command
|
||||||
+ (
|
+ (
|
||||||
@@ -508,7 +460,6 @@ def restore_data_source_dump(
|
|||||||
+ (('--host', hostname) if hostname else ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(port)) if port else ())
|
+ (('--port', str(port)) if port else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or port else ())
|
+ (('--protocol', 'tcp') if hostname or port else ())
|
||||||
+ (('--socket', socket_path) if socket_path else ())
|
|
||||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||||
+ (('--ssl',) if tls is True else ())
|
+ (('--ssl',) if tls is True else ())
|
||||||
+ (('--skip-ssl',) if tls is False else ())
|
+ (('--skip-ssl',) if tls is False else ())
|
||||||
@@ -524,14 +475,10 @@ def restore_data_source_dump(
|
|||||||
|
|
||||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
# 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.
|
# if the restore paths don't exist in the archive.
|
||||||
tuple(
|
execute_command_with_processes(
|
||||||
execute_command_with_processes(
|
restore_command,
|
||||||
restore_command,
|
[extract_process],
|
||||||
[extract_process],
|
output_log_level=logging.DEBUG,
|
||||||
output_log_level=logging.DEBUG,
|
input_file=extract_process.stdout,
|
||||||
input_file=extract_process.stdout,
|
environment=environment,
|
||||||
environment=environment,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
borg_local_path=config.get('local_path', 'borg'),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import shlex
|
|||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.hooks.credential.parse
|
import borgmatic.hooks.credential.parse
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
from borgmatic.execute import execute_command, execute_command_with_processes
|
from borgmatic.execute import execute_command, execute_command_with_processes
|
||||||
from borgmatic.hooks.data_source import config as database_config
|
from borgmatic.hooks.data_source import config as database_config
|
||||||
from borgmatic.hooks.data_source import dump
|
from borgmatic.hooks.data_source import dump
|
||||||
@@ -90,28 +89,18 @@ def dump_data_sources(
|
|||||||
|
|
||||||
if dump_format == 'directory':
|
if dump_format == 'directory':
|
||||||
dump.create_parent_directory_for_dump(dump_filename)
|
dump.create_parent_directory_for_dump(dump_filename)
|
||||||
execute_command( # noqa: S604
|
execute_command(command, shell=True) # noqa: S604
|
||||||
command,
|
|
||||||
shell=True,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
dump.create_named_pipe_for_dump(dump_filename)
|
dump.create_named_pipe_for_dump(dump_filename)
|
||||||
processes.append(
|
processes.append(
|
||||||
execute_command( # noqa: S604
|
execute_command(command, shell=True, run_to_completion=False), # noqa: S604
|
||||||
command,
|
|
||||||
shell=True,
|
|
||||||
run_to_completion=False,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
dump.write_data_source_dumps_metadata(
|
dump.write_data_source_dumps_metadata(
|
||||||
borgmatic_runtime_directory, 'mongodb_databases', dumps_metadata
|
borgmatic_runtime_directory, 'mongodb_databases', dumps_metadata
|
||||||
)
|
)
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
patterns.append(
|
||||||
patterns,
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
borgmatic.borg.pattern.Pattern(
|
||||||
os.path.join(borgmatic_runtime_directory, 'mongodb_databases'),
|
os.path.join(borgmatic_runtime_directory, 'mongodb_databases'),
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
||||||
@@ -196,7 +185,6 @@ def remove_data_source_dumps(
|
|||||||
databases,
|
databases,
|
||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
patterns,
|
|
||||||
dry_run,
|
dry_run,
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
@@ -212,10 +200,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||||
@@ -225,24 +209,16 @@ def make_data_source_dump_patterns(
|
|||||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
|
||||||
),
|
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_runtime_directory),
|
make_dump_path(borgmatic_runtime_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_source_directory),
|
make_dump_path(borgmatic_source_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -289,15 +265,11 @@ def restore_data_source_dump(
|
|||||||
|
|
||||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
# 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.
|
# if the restore paths don't exist in the archive.
|
||||||
tuple(
|
execute_command_with_processes(
|
||||||
execute_command_with_processes(
|
restore_command,
|
||||||
restore_command,
|
[extract_process] if extract_process else [],
|
||||||
[extract_process] if extract_process else [],
|
output_log_level=logging.DEBUG,
|
||||||
output_log_level=logging.DEBUG,
|
input_file=extract_process.stdout if extract_process else None,
|
||||||
input_file=extract_process.stdout if extract_process else None,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
borg_local_path=config.get('local_path', 'borg'),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
@@ -5,7 +6,6 @@ import shlex
|
|||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.hooks.credential.parse
|
import borgmatic.hooks.credential.parse
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
import borgmatic.hooks.data_source.mariadb
|
import borgmatic.hooks.data_source.mariadb
|
||||||
from borgmatic.execute import (
|
from borgmatic.execute import (
|
||||||
execute_command,
|
execute_command,
|
||||||
@@ -35,14 +35,7 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
names to dump. In the case of "all", query for the names of databases on the configured host and
|
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.
|
return them, excluding any system databases that will cause problems during restore.
|
||||||
'''
|
'''
|
||||||
skip_names = database.get('skip_names')
|
|
||||||
|
|
||||||
if database['name'] != 'all':
|
if database['name'] != 'all':
|
||||||
if skip_names:
|
|
||||||
logger.warning(
|
|
||||||
f'For MySQL database {database["name"]}, ignoring the "skip_names" option, which is only supported for database "all"'
|
|
||||||
)
|
|
||||||
|
|
||||||
return (database['name'],)
|
return (database['name'],)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
@@ -56,8 +49,6 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
)
|
)
|
||||||
password_transport = database.get('password_transport', 'pipe')
|
password_transport = database.get('password_transport', 'pipe')
|
||||||
hostname = database_config.resolve_database_option('hostname', database)
|
hostname = database_config.resolve_database_option('hostname', database)
|
||||||
socket_path = database.get('socket_path')
|
|
||||||
|
|
||||||
show_command = (
|
show_command = (
|
||||||
mysql_show_command
|
mysql_show_command
|
||||||
+ (
|
+ (
|
||||||
@@ -73,7 +64,6 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
+ (('--host', hostname) if hostname else ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
||||||
+ (('--socket', socket_path) if socket_path else ())
|
|
||||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||||
+ (('--ssl',) if database.get('tls') is True else ())
|
+ (('--ssl',) if database.get('tls') is True else ())
|
||||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||||
@@ -83,20 +73,12 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
|||||||
|
|
||||||
logger.debug('Querying for "all" MySQL databases to dump')
|
logger.debug('Querying for "all" MySQL databases to dump')
|
||||||
|
|
||||||
if skip_names:
|
show_output = execute_command_and_capture_output(show_command, environment=environment)
|
||||||
logger.debug(f'Skipping database names: {", ".join(skip_names)}')
|
|
||||||
|
|
||||||
show_lines = execute_command_and_capture_output(
|
|
||||||
show_command,
|
|
||||||
environment=environment,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
show_name.strip()
|
show_name
|
||||||
for show_name in show_lines
|
for show_name in show_output.strip().splitlines()
|
||||||
if show_name not in SYSTEM_DATABASE_NAMES
|
if show_name not in SYSTEM_DATABASE_NAMES
|
||||||
if not skip_names or show_name not in skip_names
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -142,8 +124,6 @@ def execute_dump_command(
|
|||||||
)
|
)
|
||||||
password_transport = database.get('password_transport', 'pipe')
|
password_transport = database.get('password_transport', 'pipe')
|
||||||
hostname = database_config.resolve_database_option('hostname', database)
|
hostname = database_config.resolve_database_option('hostname', database)
|
||||||
socket_path = database.get('socket_path')
|
|
||||||
|
|
||||||
dump_command = (
|
dump_command = (
|
||||||
mysql_dump_command
|
mysql_dump_command
|
||||||
+ (
|
+ (
|
||||||
@@ -157,15 +137,13 @@ def execute_dump_command(
|
|||||||
)
|
)
|
||||||
+ extra_options
|
+ extra_options
|
||||||
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
|
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
|
||||||
+ ('--single-transaction',)
|
|
||||||
+ (('--host', hostname) if hostname else ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
+ (('--protocol', 'tcp') if hostname or 'port' in database else ())
|
||||||
+ (('--socket', socket_path) if socket_path else ())
|
|
||||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||||
+ (('--ssl',) if database.get('tls') is True else ())
|
+ (('--ssl',) if database.get('tls') is True else ())
|
||||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||||
+ ('--databases', '--events', '--routines', '--all-tablespaces')
|
+ ('--databases',)
|
||||||
+ database_names
|
+ database_names
|
||||||
+ ('--result-file', dump_filename)
|
+ ('--result-file', dump_filename)
|
||||||
)
|
)
|
||||||
@@ -180,7 +158,6 @@ def execute_dump_command(
|
|||||||
dump_command,
|
dump_command,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
run_to_completion=False,
|
run_to_completion=False,
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -265,7 +242,8 @@ def dump_data_sources(
|
|||||||
database.get('container'),
|
database.get('container'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
renamed_database = dict(database, name=database_name)
|
renamed_database = copy.copy(database)
|
||||||
|
renamed_database['name'] = database_name
|
||||||
processes.append(
|
processes.append(
|
||||||
execute_dump_command(
|
execute_dump_command(
|
||||||
renamed_database,
|
renamed_database,
|
||||||
@@ -308,8 +286,7 @@ def dump_data_sources(
|
|||||||
dump.write_data_source_dumps_metadata(
|
dump.write_data_source_dumps_metadata(
|
||||||
borgmatic_runtime_directory, 'mysql_databases', dumps_metadata
|
borgmatic_runtime_directory, 'mysql_databases', dumps_metadata
|
||||||
)
|
)
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
patterns.append(
|
||||||
patterns,
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
borgmatic.borg.pattern.Pattern(
|
||||||
os.path.join(borgmatic_runtime_directory, 'mysql_databases'),
|
os.path.join(borgmatic_runtime_directory, 'mysql_databases'),
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
||||||
@@ -323,7 +300,6 @@ def remove_data_source_dumps(
|
|||||||
databases,
|
databases,
|
||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
patterns,
|
|
||||||
dry_run,
|
dry_run,
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
@@ -339,10 +315,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||||
@@ -352,24 +324,16 @@ def make_data_source_dump_patterns(
|
|||||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
|
||||||
),
|
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_runtime_directory),
|
make_dump_path(borgmatic_runtime_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_source_directory),
|
make_dump_path(borgmatic_source_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -396,7 +360,6 @@ def restore_data_source_dump(
|
|||||||
port = database_config.resolve_database_option(
|
port = database_config.resolve_database_option(
|
||||||
'port', data_source, connection_params, restore=True
|
'port', data_source, connection_params, restore=True
|
||||||
)
|
)
|
||||||
socket_path = database_config.resolve_database_option('socket_path', data_source, restore=True)
|
|
||||||
tls = database_config.resolve_database_option('tls', data_source, restore=True)
|
tls = database_config.resolve_database_option('tls', data_source, restore=True)
|
||||||
username = borgmatic.hooks.credential.parse.resolve_credential(
|
username = borgmatic.hooks.credential.parse.resolve_credential(
|
||||||
database_config.resolve_database_option(
|
database_config.resolve_database_option(
|
||||||
@@ -418,7 +381,6 @@ def restore_data_source_dump(
|
|||||||
borgmatic.hooks.data_source.mariadb.parse_extra_options(data_source.get('restore_options'))
|
borgmatic.hooks.data_source.mariadb.parse_extra_options(data_source.get('restore_options'))
|
||||||
)
|
)
|
||||||
password_transport = data_source.get('password_transport', 'pipe')
|
password_transport = data_source.get('password_transport', 'pipe')
|
||||||
|
|
||||||
restore_command = (
|
restore_command = (
|
||||||
mysql_restore_command
|
mysql_restore_command
|
||||||
+ (
|
+ (
|
||||||
@@ -435,7 +397,6 @@ def restore_data_source_dump(
|
|||||||
+ (('--host', hostname) if hostname else ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(port)) if port else ())
|
+ (('--port', str(port)) if port else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or port else ())
|
+ (('--protocol', 'tcp') if hostname or port else ())
|
||||||
+ (('--socket', socket_path) if socket_path else ())
|
|
||||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||||
+ (('--ssl',) if tls is True else ())
|
+ (('--ssl',) if tls is True else ())
|
||||||
+ (('--skip-ssl',) if tls is False else ())
|
+ (('--skip-ssl',) if tls is False else ())
|
||||||
@@ -451,14 +412,10 @@ def restore_data_source_dump(
|
|||||||
|
|
||||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
# 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.
|
# if the restore paths don't exist in the archive.
|
||||||
tuple(
|
execute_command_with_processes(
|
||||||
execute_command_with_processes(
|
restore_command,
|
||||||
restore_command,
|
[extract_process],
|
||||||
[extract_process],
|
output_log_level=logging.DEBUG,
|
||||||
output_log_level=logging.DEBUG,
|
input_file=extract_process.stdout,
|
||||||
input_file=extract_process.stdout,
|
environment=environment,
|
||||||
environment=environment,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
borg_local_path=config.get('local_path', 'borg'),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import shlex
|
|||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.hooks.credential.parse
|
import borgmatic.hooks.credential.parse
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
from borgmatic.execute import (
|
from borgmatic.execute import (
|
||||||
execute_command,
|
execute_command,
|
||||||
execute_command_and_capture_output,
|
execute_command_and_capture_output,
|
||||||
@@ -100,22 +99,14 @@ def database_names_to_dump(database, config, environment, dry_run):
|
|||||||
if 'username' in database
|
if 'username' in database
|
||||||
else ()
|
else ()
|
||||||
)
|
)
|
||||||
+ (
|
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
|
||||||
tuple(shlex.quote(part) for part in shlex.split(database['list_options']))
|
|
||||||
if 'list_options' in database
|
|
||||||
else ()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
logger.debug('Querying for "all" PostgreSQL databases to dump')
|
logger.debug('Querying for "all" PostgreSQL databases to dump')
|
||||||
list_lines = execute_command_and_capture_output(
|
list_output = execute_command_and_capture_output(list_command, environment=environment)
|
||||||
list_command,
|
|
||||||
environment=environment,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
row[0]
|
row[0]
|
||||||
for row in csv.reader(list_lines, delimiter=',', quotechar='"')
|
for row in csv.reader(list_output.splitlines(), delimiter=',', quotechar='"')
|
||||||
if row[0] not in EXCLUDED_DATABASE_NAMES
|
if row[0] not in EXCLUDED_DATABASE_NAMES
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,7 +221,7 @@ def dump_data_sources(
|
|||||||
+ (('--compress', shlex.quote(str(compression))) if compression is not None else ())
|
+ (('--compress', shlex.quote(str(compression))) if compression is not None else ())
|
||||||
+ (('--file', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
|
+ (('--file', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
|
||||||
+ (
|
+ (
|
||||||
tuple(shlex.quote(part) for part in shlex.split(database['options']))
|
tuple(shlex.quote(option) for option in database['options'].split(' '))
|
||||||
if 'options' in database
|
if 'options' in database
|
||||||
else ()
|
else ()
|
||||||
)
|
)
|
||||||
@@ -253,7 +244,6 @@ def dump_data_sources(
|
|||||||
command,
|
command,
|
||||||
shell=True,
|
shell=True,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
dump.create_named_pipe_for_dump(dump_filename)
|
dump.create_named_pipe_for_dump(dump_filename)
|
||||||
@@ -263,7 +253,6 @@ def dump_data_sources(
|
|||||||
shell=True,
|
shell=True,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
run_to_completion=False,
|
run_to_completion=False,
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -271,8 +260,7 @@ def dump_data_sources(
|
|||||||
dump.write_data_source_dumps_metadata(
|
dump.write_data_source_dumps_metadata(
|
||||||
borgmatic_runtime_directory, 'postgresql_databases', dumps_metadata
|
borgmatic_runtime_directory, 'postgresql_databases', dumps_metadata
|
||||||
)
|
)
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
patterns.append(
|
||||||
patterns,
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
borgmatic.borg.pattern.Pattern(
|
||||||
os.path.join(borgmatic_runtime_directory, 'postgresql_databases'),
|
os.path.join(borgmatic_runtime_directory, 'postgresql_databases'),
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
||||||
@@ -286,7 +274,6 @@ def remove_data_source_dumps(
|
|||||||
databases,
|
databases,
|
||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
patterns,
|
|
||||||
dry_run,
|
dry_run,
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
@@ -306,10 +293,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||||
@@ -319,24 +302,16 @@ def make_data_source_dump_patterns(
|
|||||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
|
||||||
),
|
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_runtime_directory),
|
make_dump_path(borgmatic_runtime_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_source_directory),
|
make_dump_path(borgmatic_source_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -397,7 +372,7 @@ def restore_data_source_dump(
|
|||||||
+ (('--username', username) if username else ())
|
+ (('--username', username) if username else ())
|
||||||
+ (('--dbname', data_source['name']) if not all_databases else ())
|
+ (('--dbname', data_source['name']) if not all_databases else ())
|
||||||
+ (
|
+ (
|
||||||
tuple(shlex.quote(part) for part in shlex.split(data_source['analyze_options']))
|
tuple(data_source['analyze_options'].split(' '))
|
||||||
if 'analyze_options' in data_source
|
if 'analyze_options' in data_source
|
||||||
else ()
|
else ()
|
||||||
)
|
)
|
||||||
@@ -418,7 +393,7 @@ def restore_data_source_dump(
|
|||||||
+ (('--username', username) if username else ())
|
+ (('--username', username) if username else ())
|
||||||
+ (('--no-owner',) if data_source.get('no_owner', False) else ())
|
+ (('--no-owner',) if data_source.get('no_owner', False) else ())
|
||||||
+ (
|
+ (
|
||||||
tuple(shlex.quote(part) for part in shlex.split(data_source['restore_options']))
|
tuple(data_source['restore_options'].split(' '))
|
||||||
if 'restore_options' in data_source
|
if 'restore_options' in data_source
|
||||||
else ()
|
else ()
|
||||||
)
|
)
|
||||||
@@ -438,19 +413,11 @@ def restore_data_source_dump(
|
|||||||
|
|
||||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
# 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.
|
# if the restore paths don't exist in the archive.
|
||||||
tuple(
|
execute_command_with_processes(
|
||||||
execute_command_with_processes(
|
restore_command,
|
||||||
restore_command,
|
[extract_process] if extract_process else [],
|
||||||
[extract_process] if extract_process else [],
|
output_log_level=logging.DEBUG,
|
||||||
output_log_level=logging.DEBUG,
|
input_file=extract_process.stdout if extract_process else None,
|
||||||
input_file=extract_process.stdout if extract_process else None,
|
|
||||||
environment=environment,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
borg_local_path=config.get('local_path', 'borg'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
execute_command(
|
|
||||||
analyze_command,
|
|
||||||
environment=environment,
|
environment=environment,
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
)
|
)
|
||||||
|
execute_command(analyze_command, environment=environment)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import shlex
|
|||||||
|
|
||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
from borgmatic.execute import execute_command, execute_command_with_processes
|
from borgmatic.execute import execute_command, execute_command_with_processes
|
||||||
from borgmatic.hooks.data_source import dump
|
from borgmatic.hooks.data_source import dump
|
||||||
|
|
||||||
@@ -86,7 +85,6 @@ def dump_data_sources(
|
|||||||
)
|
)
|
||||||
command = (
|
command = (
|
||||||
*sqlite_command,
|
*sqlite_command,
|
||||||
'-bail',
|
|
||||||
shlex.quote(database_path),
|
shlex.quote(database_path),
|
||||||
'.dump',
|
'.dump',
|
||||||
'>',
|
'>',
|
||||||
@@ -101,20 +99,14 @@ def dump_data_sources(
|
|||||||
|
|
||||||
dump.create_named_pipe_for_dump(dump_filename)
|
dump.create_named_pipe_for_dump(dump_filename)
|
||||||
processes.append(
|
processes.append(
|
||||||
execute_command( # noqa: S604
|
execute_command(command, shell=True, run_to_completion=False), # noqa: S604
|
||||||
command,
|
|
||||||
shell=True,
|
|
||||||
run_to_completion=False,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
dump.write_data_source_dumps_metadata(
|
dump.write_data_source_dumps_metadata(
|
||||||
borgmatic_runtime_directory, 'sqlite_databases', dumps_metadata
|
borgmatic_runtime_directory, 'sqlite_databases', dumps_metadata
|
||||||
)
|
)
|
||||||
borgmatic.hooks.data_source.config.inject_pattern(
|
patterns.append(
|
||||||
patterns,
|
|
||||||
borgmatic.borg.pattern.Pattern(
|
borgmatic.borg.pattern.Pattern(
|
||||||
os.path.join(borgmatic_runtime_directory, 'sqlite_databases'),
|
os.path.join(borgmatic_runtime_directory, 'sqlite_databases'),
|
||||||
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
source=borgmatic.borg.pattern.Pattern_source.HOOK,
|
||||||
@@ -128,7 +120,6 @@ def remove_data_source_dumps(
|
|||||||
databases,
|
databases,
|
||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
patterns,
|
|
||||||
dry_run,
|
dry_run,
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
@@ -144,10 +135,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||||
@@ -157,24 +144,16 @@ def make_data_source_dump_patterns(
|
|||||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'),
|
||||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
|
||||||
),
|
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_runtime_directory),
|
make_dump_path(borgmatic_runtime_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
dump.make_data_source_dump_filename(
|
dump.make_data_source_dump_filename(
|
||||||
make_dump_path(borgmatic_source_directory),
|
make_dump_path(borgmatic_source_directory),
|
||||||
name,
|
name,
|
||||||
hostname,
|
label='*',
|
||||||
port,
|
|
||||||
container,
|
|
||||||
label,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -214,17 +193,12 @@ def restore_data_source_dump(
|
|||||||
shlex.quote(part)
|
shlex.quote(part)
|
||||||
for part in shlex.split(data_source.get('sqlite_restore_command') or 'sqlite3')
|
for part in shlex.split(data_source.get('sqlite_restore_command') or 'sqlite3')
|
||||||
)
|
)
|
||||||
restore_command = (*sqlite_restore_command, '-bail', shlex.quote(database_path))
|
restore_command = (*sqlite_restore_command, shlex.quote(database_path))
|
||||||
|
|
||||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
# 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.
|
# if the restore paths don't exist in the archive.
|
||||||
tuple(
|
execute_command_with_processes(
|
||||||
execute_command_with_processes(
|
restore_command,
|
||||||
restore_command,
|
[extract_process],
|
||||||
[extract_process],
|
output_log_level=logging.DEBUG,
|
||||||
output_log_level=logging.DEBUG,
|
input_file=extract_process.stdout,
|
||||||
input_file=extract_process.stdout,
|
|
||||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
|
||||||
borg_local_path=config.get('local_path', 'borg'),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import collections
|
|||||||
import glob
|
import glob
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import operator
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -10,7 +9,6 @@ import subprocess
|
|||||||
import borgmatic.borg.pattern
|
import borgmatic.borg.pattern
|
||||||
import borgmatic.config.paths
|
import borgmatic.config.paths
|
||||||
import borgmatic.execute
|
import borgmatic.execute
|
||||||
import borgmatic.hooks.data_source.config
|
|
||||||
import borgmatic.hooks.data_source.snapshot
|
import borgmatic.hooks.data_source.snapshot
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -46,7 +44,7 @@ def get_datasets_to_backup(zfs_command, patterns):
|
|||||||
|
|
||||||
Return the result as a sequence of Dataset instances, sorted by mount point.
|
Return the result as a sequence of Dataset instances, sorted by mount point.
|
||||||
'''
|
'''
|
||||||
list_lines = borgmatic.execute.execute_command_and_capture_output(
|
list_output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
(
|
(
|
||||||
*zfs_command.split(' '),
|
*zfs_command.split(' '),
|
||||||
'list',
|
'list',
|
||||||
@@ -66,13 +64,13 @@ def get_datasets_to_backup(zfs_command, patterns):
|
|||||||
datasets = sorted(
|
datasets = sorted(
|
||||||
(
|
(
|
||||||
Dataset(dataset_name, mount_point, (user_property_value == 'auto'), ())
|
Dataset(dataset_name, mount_point, (user_property_value == 'auto'), ())
|
||||||
for line in list_lines
|
for line in list_output.splitlines()
|
||||||
for (dataset_name, mount_point, can_mount, user_property_value) in (
|
for (dataset_name, mount_point, can_mount, user_property_value) in (
|
||||||
line.rstrip().split('\t'),
|
line.rstrip().split('\t'),
|
||||||
)
|
)
|
||||||
# Skip datasets that are marked "canmount=off", because mounting their snapshots will
|
# Skip datasets that are marked "canmount=off", because mounting their snapshots will
|
||||||
# result in completely empty mount points—thereby preventing us from backing them up.
|
# result in completely empty mount points—thereby preventing us from backing them up.
|
||||||
if can_mount != 'off'
|
if can_mount == 'on'
|
||||||
),
|
),
|
||||||
key=lambda dataset: dataset.mount_point,
|
key=lambda dataset: dataset.mount_point,
|
||||||
reverse=True,
|
reverse=True,
|
||||||
@@ -124,10 +122,9 @@ def get_datasets_to_backup(zfs_command, patterns):
|
|||||||
|
|
||||||
def get_all_dataset_mount_points(zfs_command):
|
def get_all_dataset_mount_points(zfs_command):
|
||||||
'''
|
'''
|
||||||
Given a ZFS command to run, return a dict from ZFS dataset name to mount point (reverse sorted
|
Given a ZFS command to run, return all ZFS datasets as a sequence of sorted mount points.
|
||||||
by mount point).
|
|
||||||
'''
|
'''
|
||||||
list_lines = borgmatic.execute.execute_command_and_capture_output(
|
list_output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
(
|
(
|
||||||
*zfs_command.split(' '),
|
*zfs_command.split(' '),
|
||||||
'list',
|
'list',
|
||||||
@@ -135,23 +132,19 @@ def get_all_dataset_mount_points(zfs_command):
|
|||||||
'-t',
|
'-t',
|
||||||
'filesystem',
|
'filesystem',
|
||||||
'-o',
|
'-o',
|
||||||
'name,mountpoint',
|
'mountpoint',
|
||||||
),
|
),
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return dict(
|
return tuple(
|
||||||
sorted(
|
sorted(
|
||||||
(
|
{
|
||||||
(dataset_name, mount_point)
|
mount_point
|
||||||
for line in list_lines
|
for line in list_output.splitlines()
|
||||||
for (dataset_name, mount_point) in (line.rstrip().split('\t'),)
|
for mount_point in (line.rstrip(),)
|
||||||
if mount_point != 'none'
|
if mount_point != 'none'
|
||||||
),
|
},
|
||||||
key=operator.itemgetter(1),
|
|
||||||
# Reversing the sorted datasets ensures that we unmount the longer mount point paths of
|
|
||||||
# child datasets before the shorter mount point paths of parent datasets.
|
|
||||||
reverse=True,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -313,7 +306,11 @@ def dump_data_sources(
|
|||||||
normalized_runtime_directory,
|
normalized_runtime_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
borgmatic.hooks.data_source.config.replace_pattern(patterns, pattern, snapshot_pattern)
|
# Attempt to update the pattern in place, since pattern order matters to Borg.
|
||||||
|
try:
|
||||||
|
patterns[patterns.index(pattern)] = snapshot_pattern
|
||||||
|
except ValueError:
|
||||||
|
patterns.append(snapshot_pattern)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -350,7 +347,7 @@ def get_all_snapshots(zfs_command):
|
|||||||
Given a ZFS command to run, return all ZFS snapshots as a sequence of full snapshot names of the
|
Given a ZFS command to run, return all ZFS snapshots as a sequence of full snapshot names of the
|
||||||
form "dataset@snapshot".
|
form "dataset@snapshot".
|
||||||
'''
|
'''
|
||||||
list_lines = borgmatic.execute.execute_command_and_capture_output(
|
list_output = borgmatic.execute.execute_command_and_capture_output(
|
||||||
(
|
(
|
||||||
*tuple(zfs_command.split(' ')),
|
*tuple(zfs_command.split(' ')),
|
||||||
'list',
|
'list',
|
||||||
@@ -363,15 +360,15 @@ def get_all_snapshots(zfs_command):
|
|||||||
close_fds=True,
|
close_fds=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return tuple(line.rstrip() for line in list_lines)
|
return tuple(line.rstrip() for line in list_output.splitlines())
|
||||||
|
|
||||||
|
|
||||||
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, patterns, dry_run): # noqa: PLR0912
|
def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, dry_run): # noqa: PLR0912
|
||||||
'''
|
'''
|
||||||
Given a ZFS configuration dict, a configuration dict, the borgmatic runtime directory, the
|
Given a ZFS configuration dict, a configuration dict, the borgmatic runtime directory, and
|
||||||
configured patterns, and whether this is a dry run, unmount and destroy any ZFS snapshots
|
whether this is a dry run, unmount and destroy any ZFS snapshots created by borgmatic. If this
|
||||||
created by borgmatic. If this is a dry run or ZFS isn't configured in borgmatic's configuration,
|
is a dry run or ZFS isn't configured in borgmatic's configuration, then don't actually remove
|
||||||
then don't actually remove anything.
|
anything.
|
||||||
'''
|
'''
|
||||||
if hook_config is None:
|
if hook_config is None:
|
||||||
return
|
return
|
||||||
@@ -382,8 +379,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
|||||||
zfs_command = hook_config.get('zfs_command', 'zfs')
|
zfs_command = hook_config.get('zfs_command', 'zfs')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dataset_name_to_mount_point = get_all_dataset_mount_points(zfs_command)
|
dataset_mount_points = get_all_dataset_mount_points(zfs_command)
|
||||||
full_snapshot_names = get_all_snapshots(zfs_command)
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.debug(f'Could not find "{zfs_command}" command')
|
logger.debug(f'Could not find "{zfs_command}" command')
|
||||||
return
|
return
|
||||||
@@ -400,20 +396,19 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
|||||||
)
|
)
|
||||||
logger.debug(f'Looking for snapshots to remove in {snapshots_glob}{dry_run_label}')
|
logger.debug(f'Looking for snapshots to remove in {snapshots_glob}{dry_run_label}')
|
||||||
umount_command = hook_config.get('umount_command', 'umount')
|
umount_command = hook_config.get('umount_command', 'umount')
|
||||||
snapshot_dataset_names = {
|
|
||||||
full_snapshot_name.split('@')[0] for full_snapshot_name in full_snapshot_names
|
|
||||||
}
|
|
||||||
|
|
||||||
for snapshots_directory in glob.glob(snapshots_glob):
|
for snapshots_directory in glob.glob(snapshots_glob):
|
||||||
if not os.path.isdir(snapshots_directory):
|
if not os.path.isdir(snapshots_directory):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for dataset_name, mount_point in dataset_name_to_mount_point.items():
|
# Reversing the sorted datasets ensures that we unmount the longer mount point paths of
|
||||||
|
# child datasets before the shorter mount point paths of parent datasets.
|
||||||
|
for mount_point in reversed(dataset_mount_points):
|
||||||
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
|
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
|
||||||
|
|
||||||
# If this dataset name does not correspond to a known snapshot, then this is probably
|
# If the snapshot mount path is empty, this is probably just a "shadow" of a nested
|
||||||
# just a "shadow" of a nested dataset and therefore there's nothing to unmount.
|
# dataset and therefore there's nothing to unmount.
|
||||||
if not os.path.isdir(snapshot_mount_path) or dataset_name not in snapshot_dataset_names:
|
if not os.path.isdir(snapshot_mount_path) or not os.listdir(snapshot_mount_path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# This might fail if the path is already mounted, but we swallow errors here since we'll
|
# This might fail if the path is already mounted, but we swallow errors here since we'll
|
||||||
@@ -443,6 +438,8 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
|||||||
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
|
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
|
||||||
|
|
||||||
# Destroy snapshots.
|
# Destroy snapshots.
|
||||||
|
full_snapshot_names = get_all_snapshots(zfs_command)
|
||||||
|
|
||||||
for full_snapshot_name in full_snapshot_names:
|
for full_snapshot_name in full_snapshot_names:
|
||||||
# Only destroy snapshots that borgmatic actually created!
|
# Only destroy snapshots that borgmatic actually created!
|
||||||
if not full_snapshot_name.split('@')[-1].startswith(BORGMATIC_SNAPSHOT_PREFIX):
|
if not full_snapshot_name.split('@')[-1].startswith(BORGMATIC_SNAPSHOT_PREFIX):
|
||||||
@@ -459,10 +456,6 @@ def make_data_source_dump_patterns(
|
|||||||
config,
|
config,
|
||||||
borgmatic_runtime_directory,
|
borgmatic_runtime_directory,
|
||||||
name=None,
|
name=None,
|
||||||
hostname=None,
|
|
||||||
port=None,
|
|
||||||
container=None,
|
|
||||||
label=None,
|
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
Restores aren't implemented, because stored files can be extracted directly with "extract".
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
|
import borgmatic.hooks.command
|
||||||
import borgmatic.hooks.credential
|
import borgmatic.hooks.credential
|
||||||
import borgmatic.hooks.data_source
|
import borgmatic.hooks.data_source
|
||||||
import borgmatic.hooks.monitoring
|
import borgmatic.hooks.monitoring
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
|||||||
we can send them all to an Apprise notification service upon a finish or failure state. But skip
|
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.
|
this if the "send_logs" option is false.
|
||||||
'''
|
'''
|
||||||
if hook_config.get('send_logs', False) is False:
|
if hook_config.get('send_logs') is False:
|
||||||
return
|
return
|
||||||
|
|
||||||
logs_size_limit = max(
|
logs_size_limit = max(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
|||||||
we can send them all to Healthchecks upon a finish or failure state. But skip this if the
|
we can send them all to Healthchecks upon a finish or failure state. But skip this if the
|
||||||
"send_logs" option is false.
|
"send_logs" option is false.
|
||||||
'''
|
'''
|
||||||
if hook_config.get('send_logs', False) is False:
|
if hook_config.get('send_logs') is False:
|
||||||
return
|
return
|
||||||
|
|
||||||
ping_body_limit = max(
|
ping_body_limit = max(
|
||||||
|
|||||||
@@ -23,20 +23,13 @@ MAX_BUFFER_LINES = 100
|
|||||||
|
|
||||||
class Loki_log_buffer:
|
class Loki_log_buffer:
|
||||||
'''
|
'''
|
||||||
A log buffer that allows to output the logs as Loki requests in json. Allows
|
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.
|
adding labels to the log stream and takes care of communication with loki.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, url, dry_run, tls_cert_path=None, tls_key_path=None):
|
def __init__(self, url, dry_run):
|
||||||
'''
|
|
||||||
Given a Loki URL, a dry run flag, and optional TLS certificate and key paths for mTLS authentication,
|
|
||||||
create an instance of Loki_log_buffer.
|
|
||||||
'''
|
|
||||||
|
|
||||||
self.url = url
|
self.url = url
|
||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
self.tls_cert_path = tls_cert_path
|
|
||||||
self.tls_key_path = tls_key_path
|
|
||||||
self.root = {'streams': [{'stream': {}, 'values': []}]}
|
self.root = {'streams': [{'stream': {}, 'values': []}]}
|
||||||
|
|
||||||
def add_value(self, value):
|
def add_value(self, value):
|
||||||
@@ -65,58 +58,44 @@ class Loki_log_buffer:
|
|||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
# Just empty the buffer and skip
|
# Just empty the buffer and skip
|
||||||
self.root['streams'][0]['values'] = []
|
self.root['streams'][0]['values'] = []
|
||||||
logger.info('Skipped uploading logs to Loki due to dry run')
|
logger.info('Skipped uploading logs to loki due to dry run')
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(self) == 0:
|
if len(self) == 0:
|
||||||
# Skip as there are not logs to send yet
|
# Skip as there are not logs to send yet
|
||||||
return
|
return
|
||||||
|
|
||||||
request_body = self.to_request()
|
|
||||||
self.root['streams'][0]['values'] = []
|
self.root['streams'][0]['values'] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = requests.post(
|
result = requests.post(
|
||||||
self.url,
|
self.url,
|
||||||
data=request_body,
|
data=self.to_request(),
|
||||||
timeout=TIMEOUT_SECONDS,
|
timeout=TIMEOUT_SECONDS,
|
||||||
headers={
|
headers={
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': 'borgmatic',
|
'User-Agent': 'borgmatic',
|
||||||
},
|
},
|
||||||
cert=(self.tls_cert_path, self.tls_key_path) if self.tls_cert_path else None,
|
|
||||||
)
|
)
|
||||||
result.raise_for_status()
|
result.raise_for_status()
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
logger.warning('Failed to upload logs to Loki')
|
logger.warning('Failed to upload logs to loki')
|
||||||
|
|
||||||
|
|
||||||
class Loki_log_handler(logging.Handler):
|
class Loki_log_handler(logging.Handler):
|
||||||
'''
|
'''
|
||||||
A log handler that sends logs to Loki.
|
A log handler that sends logs to loki.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, url, send_logs, log_level, dry_run, tls_cert_path=None, tls_key_path=None):
|
def __init__(self, url, dry_run):
|
||||||
'''
|
|
||||||
Given a URL to send logs to, whether all borgmatic logs should be sent (or just explicitly
|
|
||||||
added messages from this hook), the log level to use (influencing which logs get sent), and
|
|
||||||
whether this is a dry run, create an instance of Loki_log_buffer.
|
|
||||||
'''
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.buffer = Loki_log_buffer(url, dry_run)
|
||||||
self.buffer = Loki_log_buffer(
|
|
||||||
url, dry_run, tls_cert_path=tls_cert_path, tls_key_path=tls_key_path
|
|
||||||
)
|
|
||||||
self.send_logs = send_logs
|
|
||||||
self.setLevel(log_level)
|
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record):
|
||||||
'''
|
'''
|
||||||
Add a general log record from the logging module to the stream—but only if send logs is
|
Add a log record from the logging module to the stream.
|
||||||
enabled.
|
|
||||||
'''
|
'''
|
||||||
if self.send_logs:
|
self.raw(record.getMessage())
|
||||||
self.raw(record.getMessage())
|
|
||||||
|
|
||||||
def add_label(self, key, value):
|
def add_label(self, key, value):
|
||||||
'''
|
'''
|
||||||
@@ -130,40 +109,22 @@ class Loki_log_handler(logging.Handler):
|
|||||||
'''
|
'''
|
||||||
self.buffer.add_value(msg)
|
self.buffer.add_value(msg)
|
||||||
|
|
||||||
# If log sending is enabled, flush the buffer (and send data to Loki) once we accumulate
|
if len(self.buffer) > MAX_BUFFER_LINES:
|
||||||
# enough log data in the buffer. But if log sending is disabled, flush immediately so that,
|
|
||||||
# for instance, start backup notifications are sent when the backup starts instead of after
|
|
||||||
# it finishes!
|
|
||||||
if len(self.buffer) > MAX_BUFFER_LINES or not self.send_logs:
|
|
||||||
self.buffer.flush()
|
self.buffer.flush()
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
'''
|
'''
|
||||||
Send the logs to Loki and empty the buffer.
|
Send the logs to loki and empty the buffer.
|
||||||
'''
|
'''
|
||||||
self.buffer.flush()
|
self.buffer.flush()
|
||||||
|
|
||||||
|
|
||||||
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
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.
|
Add a handler to the root logger to regularly send the logs to loki.
|
||||||
'''
|
'''
|
||||||
url = hook_config.get('url')
|
url = hook_config.get('url')
|
||||||
tls = hook_config.get('tls', {})
|
loki = Loki_log_handler(url, dry_run)
|
||||||
|
|
||||||
if bool(tls.get('cert_path')) != bool(tls.get('key_path')):
|
|
||||||
raise ValueError(
|
|
||||||
'Invalid Loki TLS configuration: cert_path and key_path must both be set or both be unset'
|
|
||||||
)
|
|
||||||
|
|
||||||
loki = Loki_log_handler(
|
|
||||||
url,
|
|
||||||
hook_config.get('send_logs', False),
|
|
||||||
monitoring_log_level,
|
|
||||||
dry_run,
|
|
||||||
tls_cert_path=tls.get('cert_path'),
|
|
||||||
tls_key_path=tls.get('key_path'),
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, value in hook_config.get('labels').items():
|
for key, value in hook_config.get('labels').items():
|
||||||
if value == '__hostname':
|
if value == '__hostname':
|
||||||
@@ -175,14 +136,12 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
|||||||
else:
|
else:
|
||||||
loki.add_label(key, value)
|
loki.add_label(key, value)
|
||||||
|
|
||||||
global_logger = logging.getLogger()
|
logging.getLogger().addHandler(loki)
|
||||||
global_logger.addHandler(loki)
|
|
||||||
global_logger.setLevel(min(handler.level for handler in global_logger.handlers))
|
|
||||||
|
|
||||||
|
|
||||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
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.
|
Add an entry to the loki logger with the current state.
|
||||||
'''
|
'''
|
||||||
for handler in tuple(logging.getLogger().handlers):
|
for handler in tuple(logging.getLogger().handlers):
|
||||||
if isinstance(handler, Loki_log_handler) and state in MONITOR_STATE_TO_LOKI:
|
if isinstance(handler, Loki_log_handler) and state in MONITOR_STATE_TO_LOKI:
|
||||||
@@ -193,11 +152,9 @@ def destroy_monitor(hook_config, config, monitoring_log_level, dry_run):
|
|||||||
'''
|
'''
|
||||||
Remove the monitor handler that was added to the root logger.
|
Remove the monitor handler that was added to the root logger.
|
||||||
'''
|
'''
|
||||||
global_logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
for handler in tuple(global_logger.handlers):
|
for handler in tuple(logger.handlers):
|
||||||
if isinstance(handler, Loki_log_handler):
|
if isinstance(handler, Loki_log_handler):
|
||||||
handler.flush()
|
handler.flush()
|
||||||
global_logger.removeHandler(handler)
|
logger.removeHandler(handler)
|
||||||
|
|
||||||
global_logger.setLevel(min(handler.level for handler in global_logger.handlers))
|
|
||||||
|
|||||||
@@ -22,29 +22,6 @@ def initialize_monitor(
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def convert_string_to_array(value):
|
|
||||||
value = '' if value is None else str(value)
|
|
||||||
items = []
|
|
||||||
|
|
||||||
for item in value.split(','):
|
|
||||||
stripped = item.strip()
|
|
||||||
|
|
||||||
if stripped:
|
|
||||||
items.append(stripped)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
PRIORITY_NAME_TO_ID = {
|
|
||||||
'max': 5,
|
|
||||||
'urgent': 5,
|
|
||||||
'high': 4,
|
|
||||||
'default': 3,
|
|
||||||
'low': 2,
|
|
||||||
'min': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
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.
|
Ping the configured Ntfy topic. Use the given configuration filename in any log entries.
|
||||||
@@ -54,13 +31,13 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||||||
|
|
||||||
if state.name.lower() in run_states:
|
if state.name.lower() in run_states:
|
||||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||||
default_priority = PRIORITY_NAME_TO_ID['default']
|
|
||||||
state_config = hook_config.get(
|
state_config = hook_config.get(
|
||||||
state.name.lower(),
|
state.name.lower(),
|
||||||
{
|
{
|
||||||
'title': f'A borgmatic {state.name} event happened',
|
'title': f'A borgmatic {state.name} event happened',
|
||||||
'message': f'A borgmatic {state.name} event happened',
|
'message': f'A borgmatic {state.name} event happened',
|
||||||
'priority': default_priority,
|
'priority': 'default',
|
||||||
'tags': 'borgmatic',
|
'tags': 'borgmatic',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -69,17 +46,14 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||||||
topic = hook_config.get('topic')
|
topic = hook_config.get('topic')
|
||||||
|
|
||||||
logger.info(f'Pinging ntfy topic {topic}{dry_run_label}')
|
logger.info(f'Pinging ntfy topic {topic}{dry_run_label}')
|
||||||
logger.debug(f'Using ntfy ping URL {base_url}')
|
logger.debug(f'Using Ntfy ping URL {base_url}/{topic}')
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'borgmatic',
|
'User-Agent': 'borgmatic',
|
||||||
}
|
'X-Title': state_config.get('title'),
|
||||||
payload = {
|
'X-Message': state_config.get('message'),
|
||||||
'topic': topic,
|
'X-Priority': state_config.get('priority'),
|
||||||
'title': state_config.get('title'),
|
'X-Tags': state_config.get('tags'),
|
||||||
'message': state_config.get('message'),
|
|
||||||
'priority': PRIORITY_NAME_TO_ID.get(state_config.get('priority'), default_priority),
|
|
||||||
'tags': convert_string_to_array(state_config.get('tags')),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -107,7 +81,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||||||
'ntfy access_token is set but so is username/password, only using access_token',
|
'ntfy access_token is set but so is username/password, only using access_token',
|
||||||
)
|
)
|
||||||
|
|
||||||
headers['Authorization'] = f'Bearer {access_token}'
|
auth = requests.auth.HTTPBasicAuth('', access_token)
|
||||||
elif (username and password) is not None:
|
elif (username and password) is not None:
|
||||||
auth = requests.auth.HTTPBasicAuth(username, password)
|
auth = requests.auth.HTTPBasicAuth(username, password)
|
||||||
logger.info(f'Using basic auth with user {username} for ntfy')
|
logger.info(f'Using basic auth with user {username} for ntfy')
|
||||||
@@ -120,11 +94,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
base_url,
|
f'{base_url}/{topic}',
|
||||||
auth=auth,
|
auth=auth,
|
||||||
timeout=TIMEOUT_SECONDS,
|
timeout=TIMEOUT_SECONDS,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=payload,
|
|
||||||
)
|
)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
|||||||
we can send them all to PagerDuty upon a failure state. But skip this if the "send_logs" option
|
we can send them all to PagerDuty upon a failure state. But skip this if the "send_logs" option
|
||||||
is false.
|
is false.
|
||||||
'''
|
'''
|
||||||
if hook_config.get('send_logs', False) is False:
|
if hook_config.get('send_logs') is False:
|
||||||
return
|
return
|
||||||
|
|
||||||
ping_body_limit = max(
|
ping_body_limit = max(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import urllib
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||||
|
|
||||||
data_source_name_url = hook_config.get('data_source_name_url')
|
data_source_name_url = hook_config.get('data_source_name_url')
|
||||||
environment = hook_config.get('environment')
|
|
||||||
monitor_slug = hook_config.get('monitor_slug')
|
monitor_slug = hook_config.get('monitor_slug')
|
||||||
match = DATA_SOURCE_NAME_URL_PATTERN.match(data_source_name_url)
|
match = DATA_SOURCE_NAME_URL_PATTERN.match(data_source_name_url)
|
||||||
|
|
||||||
@@ -67,12 +65,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
|||||||
if dry_run:
|
if dry_run:
|
||||||
return
|
return
|
||||||
|
|
||||||
environment_query = f'&environment={urllib.parse.quote(environment)}' if environment else ''
|
|
||||||
|
|
||||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f'{cron_url}?status={status}{environment_query}',
|
f'{cron_url}?status={status}',
|
||||||
timeout=TIMEOUT_SECONDS,
|
timeout=TIMEOUT_SECONDS,
|
||||||
headers={'User-Agent': 'borgmatic'},
|
headers={'User-Agent': 'borgmatic'},
|
||||||
)
|
)
|
||||||
|
|||||||
+19
-105
@@ -1,9 +1,7 @@
|
|||||||
import enum
|
import enum
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import socket
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@@ -87,57 +85,6 @@ class Multi_stream_handler(logging.Handler):
|
|||||||
handler.setLevel(level)
|
handler.setLevel(level)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_JOURNALD_PRIORITY = 6
|
|
||||||
|
|
||||||
|
|
||||||
class JournaldHandler(logging.Handler):
|
|
||||||
def __init__(self, journald_socket_path):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
add_custom_log_levels()
|
|
||||||
|
|
||||||
self.journald_socket_path = journald_socket_path
|
|
||||||
self.log_level_to_journald_priority = {
|
|
||||||
logging.CRITICAL: 2,
|
|
||||||
logging.ERROR: 3,
|
|
||||||
logging.WARNING: 4,
|
|
||||||
logging.ANSWER: 5,
|
|
||||||
logging.INFO: 6,
|
|
||||||
logging.DEBUG: 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
def emit(self, record):
|
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
||||||
|
|
||||||
try:
|
|
||||||
message_parts = []
|
|
||||||
entry = dict(
|
|
||||||
MESSAGE=record.getMessage(),
|
|
||||||
PRIORITY=self.log_level_to_journald_priority.get(
|
|
||||||
record.levelno, DEFAULT_JOURNALD_PRIORITY
|
|
||||||
),
|
|
||||||
SYSLOG_IDENTIFIER='borgmatic',
|
|
||||||
SYSLOG_PID=os.getpid(),
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, value in entry.items():
|
|
||||||
encoded_key = key.upper().encode('utf-8')
|
|
||||||
encoded_value = str(value).encode('utf-8')
|
|
||||||
|
|
||||||
# Multi-line and single-line values use different formats on the wire.
|
|
||||||
if b'\n' in encoded_value:
|
|
||||||
message_parts.extend((encoded_key, b'\n'))
|
|
||||||
message_parts.extend(
|
|
||||||
(len(encoded_value).to_bytes(8, 'little'), encoded_value, b'\n')
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
message_parts.extend((encoded_key, b'=', encoded_value, b'\n'))
|
|
||||||
|
|
||||||
sock.sendto(b''.join(message_parts), self.journald_socket_path)
|
|
||||||
finally:
|
|
||||||
sock.close()
|
|
||||||
|
|
||||||
|
|
||||||
class Log_prefix_formatter(logging.Formatter):
|
class Log_prefix_formatter(logging.Formatter):
|
||||||
def __init__(self, fmt='{prefix}{message}', *args, style='{', **kwargs):
|
def __init__(self, fmt='{prefix}{message}', *args, style='{', **kwargs):
|
||||||
self.prefix = None
|
self.prefix = None
|
||||||
@@ -150,29 +97,6 @@ class Log_prefix_formatter(logging.Formatter):
|
|||||||
return super().format(record)
|
return super().format(record)
|
||||||
|
|
||||||
|
|
||||||
def log_record_to_json(record):
|
|
||||||
'''
|
|
||||||
Given a logging.LogRecord, return it as a JSON-encoded string containing relevant attributes.
|
|
||||||
'''
|
|
||||||
return json.dumps(
|
|
||||||
dict(
|
|
||||||
type='log_message',
|
|
||||||
time=record.created,
|
|
||||||
message=record.getMessage(),
|
|
||||||
levelname=record.levelname,
|
|
||||||
name=record.name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Json_formatter(logging.Formatter):
|
|
||||||
def __init__(self, fmt='{message}', *args, style='{', **kwargs):
|
|
||||||
super().__init__(*args, fmt=fmt, style=style, **kwargs)
|
|
||||||
|
|
||||||
def format(self, record): # noqa: PLR6301
|
|
||||||
return log_record_to_json(record)
|
|
||||||
|
|
||||||
|
|
||||||
class Color(enum.Enum):
|
class Color(enum.Enum):
|
||||||
RESET = 0
|
RESET = 0
|
||||||
RED = 31
|
RED = 31
|
||||||
@@ -397,10 +321,6 @@ def flush_delayed_logging(target_handlers):
|
|||||||
root_logger.removeHandler(delayed_handler)
|
root_logger.removeHandler(delayed_handler)
|
||||||
|
|
||||||
|
|
||||||
JOURNALD_SOCKET_PATH = '/run/systemd/journal/socket'
|
|
||||||
SYSLOG_PATHS = ('/dev/log', '/var/run/syslog', '/var/run/log')
|
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(
|
def configure_logging(
|
||||||
console_log_level,
|
console_log_level,
|
||||||
syslog_log_level=None,
|
syslog_log_level=None,
|
||||||
@@ -408,7 +328,6 @@ def configure_logging(
|
|||||||
monitoring_log_level=None,
|
monitoring_log_level=None,
|
||||||
log_file=None,
|
log_file=None,
|
||||||
log_file_format=None,
|
log_file_format=None,
|
||||||
log_json=False,
|
|
||||||
color_enabled=True,
|
color_enabled=True,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
@@ -445,9 +364,7 @@ def configure_logging(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if log_json:
|
if color_enabled:
|
||||||
console_handler.setFormatter(Json_formatter())
|
|
||||||
elif color_enabled:
|
|
||||||
console_handler.setFormatter(Console_color_formatter())
|
console_handler.setFormatter(Console_color_formatter())
|
||||||
else:
|
else:
|
||||||
console_handler.setFormatter(Log_prefix_formatter())
|
console_handler.setFormatter(Log_prefix_formatter())
|
||||||
@@ -456,32 +373,29 @@ def configure_logging(
|
|||||||
handlers = [console_handler]
|
handlers = [console_handler]
|
||||||
|
|
||||||
if syslog_log_level != logging.DISABLED:
|
if syslog_log_level != logging.DISABLED:
|
||||||
if os.path.exists(JOURNALD_SOCKET_PATH):
|
syslog_path = None
|
||||||
journald_handler = JournaldHandler(JOURNALD_SOCKET_PATH)
|
|
||||||
journald_handler.setLevel(syslog_log_level)
|
|
||||||
handlers.append(journald_handler)
|
|
||||||
else:
|
|
||||||
syslog_path = next(
|
|
||||||
(path for path in SYSLOG_PATHS if os.path.exists(path)),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if syslog_path:
|
if os.path.exists('/dev/log'):
|
||||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
syslog_path = '/dev/log'
|
||||||
syslog_handler.setFormatter(
|
elif os.path.exists('/var/run/syslog'):
|
||||||
Log_prefix_formatter(
|
syslog_path = '/var/run/syslog'
|
||||||
'borgmatic: {levelname} {prefix}{message}',
|
elif os.path.exists('/var/run/log'):
|
||||||
),
|
syslog_path = '/var/run/log'
|
||||||
)
|
|
||||||
syslog_handler.setLevel(syslog_log_level)
|
if syslog_path:
|
||||||
handlers.append(syslog_handler)
|
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||||
|
syslog_handler.setFormatter(
|
||||||
|
Log_prefix_formatter(
|
||||||
|
'borgmatic: {levelname} {prefix}{message}',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
syslog_handler.setLevel(syslog_log_level)
|
||||||
|
handlers.append(syslog_handler)
|
||||||
|
|
||||||
if log_file and log_file_log_level != logging.DISABLED:
|
if log_file and log_file_log_level != logging.DISABLED:
|
||||||
file_handler = logging.handlers.WatchedFileHandler(log_file)
|
file_handler = logging.handlers.WatchedFileHandler(log_file)
|
||||||
file_handler.setFormatter(
|
file_handler.setFormatter(
|
||||||
Json_formatter()
|
Log_prefix_formatter(
|
||||||
if log_json
|
|
||||||
else Log_prefix_formatter(
|
|
||||||
log_file_format or '[{asctime}] {levelname}: {prefix}{message}',
|
log_file_format or '[{asctime}] {levelname}: {prefix}{message}',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user