Deprecate the "borgmatic_source_directory" option in favor of "user_runtime_directory" and "user_state_directory" (#562). Move the default borgmatic streaming database dump and bootstrap metadata location on disk (#562). With Borg 1.4+, store database dumps and bootstrap metadata in a "/borgmatic" directory within a backup archive (#838). Add "--local-path", "--remote-path", and "--user-runtime-directory" flags to the "config bootstrap" action.

This commit is contained in:
Dan Helfman 2024-11-03 13:14:05 -08:00
parent 13878be254
commit 814cdb4b87
32 changed files with 952 additions and 371 deletions

20
NEWS
View File

@ -3,12 +3,20 @@
option.
* #609: BREAKING: Apply the "working_directory" option to all actions, not just "create". This
includes repository paths, destination paths, mount points, etc.
* #562: Deprecate the "borgmatic_source_directory" option in favor of "borgmatic_runtime_directory"
and "borgmatic_state_directory".
* #562: Deprecate the "borgmatic_source_directory" option in favor of "user_runtime_directory"
and "user_state_directory".
* #562: BREAKING: Move the default borgmatic streaming database dump and bootstrap metadata
directory from ~/.borgmatic to /run/user/$UID/borgmatic, which is more XDG-compliant. Override
this location with the new "user_runtime_directory" option. Existing archives with database dumps
at the old location are still restorable.
* #562, #638: Move the default check state directory from ~/.borgmatic to
~/.local/state/borgmatic. This is more XDG-compliant and also prevents these state files from
getting backed up (unless you include them). Override this location with the new
"borgmatic_state_directory" option.
getting backed up (unless you explicitly include them). Override this location with the new
"user_state_directory" option. After the first time you run the "check" action with borgmatic
1.9.0, you can safely delete the ~/.borgmatic directory.
* #838: With Borg 1.4+, store database dumps and bootstrap metadata in a "/borgmatic" directory
within a backup archive, so the path doesn't depend on the current user. This means that you can
now backup as one user and restore or bootstrap as another user, among other use cases.
* #902: Add loading of encrypted systemd credentials. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/#using-systemd-service-credentials
* #914: Fix a confusing apparent hang when when the repository location changes, and instead
@ -36,6 +44,10 @@
* Update the "--match-archives" and "--archive" flags to support Borg 2 series names or archive
hashes.
* Add a "--match-archives" flag to the "prune" action.
* Add "--local-path" and "--remote-path" flags to the "config bootstrap" action for setting the
Borg executable paths used for bootstrapping.
* Add a "--user-runtime-directory" flag to the "config bootstrap" action for helping borgmatic
locate the bootstrap metadata stored in an archive.
* Add a Zabbix monitoring hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook
* Add a tarball of borgmatic's HTML documentation to the packages on the project page.

View File

@ -368,7 +368,7 @@ def collect_spot_check_source_paths(
config_paths=(),
local_borg_version=local_borg_version,
global_arguments=global_arguments,
borgmatic_source_directories=(),
borgmatic_runtime_directories=(),
local_path=local_path,
remote_path=remote_path,
list_files=True,
@ -427,6 +427,7 @@ def collect_spot_check_archive_paths(
)
for (file_type, path) in (line.split(' ', 1),)
if file_type != BORG_DIRECTORY_FILE_TYPE
if pathlib.Path('/borgmatic') not in pathlib.Path(path).parents
if pathlib.Path(borgmatic_source_directory) not in pathlib.Path(path).parents
if pathlib.Path(borgmatic_runtime_directory) not in pathlib.Path(path).parents
)

View File

@ -4,51 +4,68 @@ import os
import borgmatic.borg.extract
import borgmatic.borg.repo_list
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.command
from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
logger = logging.getLogger(__name__)
def get_config_paths(bootstrap_arguments, global_arguments, local_borg_version):
def make_bootstrap_config(bootstrap_arguments):
'''
Given the bootstrap arguments as an argparse.Namespace (containing the repository and archive
name, borgmatic source directory, destination directory, and whether to strip components), the
global arguments as an argparse.Namespace (containing the dry run flag and the local borg
version), return the config paths from the manifest.json file in the borgmatic source directory
after extracting it from the repository.
Given the bootstrap arguments as an argparse.Namespace, return a corresponding config dict.
'''
return {
'ssh_command': bootstrap_arguments.ssh_command,
# In case the repo has been moved or is accessed from a different path at the point of
# bootstrapping.
'relocated_repo_access_is_ok': True,
}
def get_config_paths(archive_name, bootstrap_arguments, global_arguments, local_borg_version):
'''
Given an archive name, the bootstrap arguments as an argparse.Namespace (containing the
repository and archive name, Borg local path, Borg remote path, borgmatic runtime directory,
borgmatic source directory, destination directory, and whether to strip components), the global
arguments as an argparse.Namespace (containing the dry run flag and the local borg version),
return the config paths from the manifest.json file in the borgmatic source directory or runtime
directory after extracting it from the repository archive.
Raise ValueError if the manifest JSON is missing, can't be decoded, or doesn't contain the
expected configuration path data.
'''
borgmatic_source_directory = (
bootstrap_arguments.borgmatic_source_directory or DEFAULT_BORGMATIC_SOURCE_DIRECTORY
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(
{'borgmatic_source_directory': bootstrap_arguments.borgmatic_source_directory}
)
borgmatic_manifest_path = os.path.expanduser(
os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json')
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(
{'user_runtime_directory': bootstrap_arguments.user_runtime_directory}
)
config = {'ssh_command': bootstrap_arguments.ssh_command}
config = make_bootstrap_config(bootstrap_arguments)
extract_process = borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
bootstrap_arguments.repository,
borgmatic.borg.repo_list.resolve_archive_name(
# Probe for the manifest file in multiple locations, as the default location has moved to the
# borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
# still want to support reading the manifest from previously created archives as well.
for base_directory in ('borgmatic', borgmatic_runtime_directory, borgmatic_source_directory):
borgmatic_manifest_path = os.path.join(base_directory, 'bootstrap', 'manifest.json')
extract_process = borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
bootstrap_arguments.repository,
bootstrap_arguments.archive,
archive_name,
[borgmatic_manifest_path],
config,
local_borg_version,
global_arguments,
),
[borgmatic_manifest_path],
config,
local_borg_version,
global_arguments,
extract_to_stdout=True,
)
manifest_json = extract_process.stdout.read()
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
extract_to_stdout=True,
)
manifest_json = extract_process.stdout.read()
if not manifest_json:
if manifest_json:
break
else:
raise ValueError(
'Cannot read configuration paths from archive due to missing bootstrap manifest'
)
@ -75,27 +92,32 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
Raise ValueError if the bootstrap configuration could not be loaded.
Raise CalledProcessError or OSError if Borg could not be run.
'''
manifest_config_paths = get_config_paths(
bootstrap_arguments, global_arguments, local_borg_version
config = make_bootstrap_config(bootstrap_arguments)
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
)
manifest_config_paths = get_config_paths(
archive_name, bootstrap_arguments, global_arguments, local_borg_version
)
config = {'ssh_command': bootstrap_arguments.ssh_command}
logger.info(f"Bootstrapping config paths: {', '.join(manifest_config_paths)}")
borgmatic.borg.extract.extract_archive(
global_arguments.dry_run,
bootstrap_arguments.repository,
borgmatic.borg.repo_list.resolve_archive_name(
bootstrap_arguments.repository,
bootstrap_arguments.archive,
config,
local_borg_version,
global_arguments,
),
archive_name,
[config_path.lstrip(os.path.sep) for config_path in manifest_config_paths],
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
extract_to_stdout=False,
destination_path=bootstrap_arguments.destination,
strip_components=bootstrap_arguments.strip_components,

View File

@ -5,7 +5,7 @@ import os
import borgmatic.actions.json
import borgmatic.borg.create
import borgmatic.borg.state
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.command
import borgmatic.hooks.dispatch
@ -22,12 +22,9 @@ def create_borgmatic_manifest(config, config_paths, dry_run):
if dry_run:
return
borgmatic_source_directory = config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
borgmatic_manifest_path = os.path.expanduser(
os.path.join(borgmatic_source_directory, 'bootstrap', 'manifest.json')
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
borgmatic_manifest_path = os.path.join(
borgmatic_runtime_directory, 'bootstrap', 'manifest.json'
)
if not os.path.exists(borgmatic_manifest_path):

View File

@ -1,12 +1,15 @@
import copy
import logging
import os
import pathlib
import shutil
import tempfile
import borgmatic.borg.extract
import borgmatic.borg.list
import borgmatic.borg.mount
import borgmatic.borg.repo_list
import borgmatic.borg.state
import borgmatic.config.paths
import borgmatic.config.validate
import borgmatic.hooks.dispatch
import borgmatic.hooks.dump
@ -61,6 +64,37 @@ def get_configured_data_source(
)
def strip_path_prefix_from_extracted_dump_destination(
destination_path, borgmatic_runtime_directory
):
'''
Directory-format dump files get extracted into a temporary directory containing a path prefix
that depends how the files were stored in the archive. So, given the destination path where the
dump was extracted and the borgmatic runtime directory, move the dump files such that the
restore doesn't have to deal with that varying path prefix.
For instance, if the dump was extracted to:
/run/user/0/borgmatic/tmp1234/borgmatic/postgresql_databases/test/...
or:
/run/user/0/borgmatic/tmp1234/root/.borgmatic/postgresql_databases/test/...
then this function moves it to:
/run/user/0/borgmatic/postgresql_databases/test/...
'''
for subdirectory_path, _, _ in os.walk(destination_path):
databases_directory = os.path.basename(subdirectory_path)
if not databases_directory.endswith('_databases'):
continue
os.rename(subdirectory_path, os.path.join(borgmatic_runtime_directory, databases_directory))
break
def restore_single_data_source(
repository,
config,
@ -72,7 +106,7 @@ def restore_single_data_source(
hook_name,
data_source,
connection_params,
): # pragma: no cover
):
'''
Given (among other things) an archive name, a data source hook name, the hostname, port,
username/password as connection params, and a configured data source configuration dict, restore
@ -82,31 +116,48 @@ def restore_single_data_source(
f'{repository.get("label", repository["path"])}: Restoring data source {data_source["name"]}'
)
dump_pattern = borgmatic.hooks.dispatch.call_hooks(
'make_data_source_dump_pattern',
dump_patterns = borgmatic.hooks.dispatch.call_hooks(
'make_data_source_dump_patterns',
config,
repository['path'],
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
data_source['name'],
)[hook_name]
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
# Kick off a single data source extract to stdout.
extract_process = borgmatic.borg.extract.extract_archive(
dry_run=global_arguments.dry_run,
repository=repository['path'],
archive=archive_name,
paths=borgmatic.hooks.dump.convert_glob_patterns_to_borg_patterns([dump_pattern]),
config=config,
local_borg_version=local_borg_version,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
destination_path='/',
# A directory format dump isn't a single file, and therefore can't extract
# to stdout. In this case, the extract_process return value is None.
extract_to_stdout=bool(data_source.get('format') != 'directory'),
destination_path = (
tempfile.mkdtemp(dir=borgmatic_runtime_directory)
if data_source.get('format') == 'directory'
else None
)
try:
# Kick off a single data source extract. If using a directory format, extract to a temporary
# directory. Otheriwes extract the single dump file to stdout.
extract_process = borgmatic.borg.extract.extract_archive(
dry_run=global_arguments.dry_run,
repository=repository['path'],
archive=archive_name,
paths=[borgmatic.hooks.dump.convert_glob_patterns_to_borg_pattern(dump_patterns)],
config=config,
local_borg_version=local_borg_version,
global_arguments=global_arguments,
local_path=local_path,
remote_path=remote_path,
destination_path=destination_path,
# A directory format dump isn't a single file, and therefore can't extract
# to stdout. In this case, the extract_process return value is None.
extract_to_stdout=bool(data_source.get('format') != 'directory'),
)
if destination_path and not global_arguments.dry_run:
strip_path_prefix_from_extracted_dump_destination(
destination_path, borgmatic_runtime_directory
)
finally:
if destination_path and not global_arguments.dry_run:
shutil.rmtree(destination_path, ignore_errors=True)
# Run a single data source restore, consuming the extract stdout (if any).
borgmatic.hooks.dispatch.call_hooks(
function_name='restore_data_source_dump',
@ -135,11 +186,14 @@ def collect_archive_data_source_names(
query the archive for the names of data sources it contains as dumps and return them as a dict
from hook name to a sequence of data source names.
'''
borgmatic_source_directory = os.path.expanduser(
config.get(
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
)
).lstrip('/')
borgmatic_source_directory = str(
pathlib.Path(borgmatic.config.paths.get_borgmatic_source_directory(config))
)
borgmatic_runtime_directory = borgmatic.config.paths.get_borgmatic_runtime_directory(config)
# Probe for the data source dumps in multiple locations, as the default location has moved to
# the borgmatic runtime directory (which get stored as just "/borgmatic" with Borg 1.4+). But we
# still want to support reading dumps from previously created archives as well.
dump_paths = borgmatic.borg.list.capture_archive_listing(
repository,
archive,
@ -148,10 +202,12 @@ def collect_archive_data_source_names(
global_arguments,
list_paths=[
'sh:'
+ os.path.expanduser(
borgmatic.hooks.dump.make_data_source_dump_path(borgmatic_source_directory, pattern)
+ borgmatic.hooks.dump.make_data_source_dump_path(base_directory, '*_databases/*/*')
for base_directory in (
'borgmatic',
borgmatic_runtime_directory.lstrip('/'),
borgmatic_source_directory.lstrip('/'),
)
for pattern in ('*_databases/*/*',)
],
local_path=local_path,
remote_path=remote_path,
@ -162,17 +218,28 @@ def collect_archive_data_source_names(
archive_data_source_names = {}
for dump_path in dump_paths:
try:
(hook_name, _, data_source_name) = dump_path.split(
borgmatic_source_directory + os.path.sep, 1
)[1].split(os.path.sep)[0:3]
except (ValueError, IndexError):
if not dump_path:
continue
for base_directory in (
'borgmatic',
borgmatic_runtime_directory,
borgmatic_source_directory,
):
try:
(hook_name, _, data_source_name) = dump_path.split(base_directory + os.path.sep, 1)[
1
].split(os.path.sep)[0:3]
except (ValueError, IndexError):
pass
else:
if data_source_name not in archive_data_source_names.get(hook_name, []):
archive_data_source_names.setdefault(hook_name, []).extend([data_source_name])
break
else:
logger.warning(
f'{repository}: Ignoring invalid data source dump path "{dump_path}" in archive {archive}'
)
else:
if data_source_name not in archive_data_source_names.get(hook_name, []):
archive_data_source_names.setdefault(hook_name, []).extend([data_source_name])
return archive_data_source_names
@ -243,7 +310,7 @@ def ensure_data_sources_found(restore_names, remaining_restore_names, found_name
)
if not combined_restore_names and not found_names:
raise ValueError('No data sources were found to restore')
raise ValueError('No data source dumps were found to restore')
missing_names = sorted(set(combined_restore_names) - set(found_names))
if missing_names:

View File

@ -9,7 +9,7 @@ import textwrap
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags, state
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import (
DO_NOT_CAPTURE,
execute_command,
@ -221,18 +221,13 @@ def make_list_filter_flags(local_borg_version, dry_run):
return f'{base_flags}-'
def collect_borgmatic_source_directories(borgmatic_source_directory):
def collect_borgmatic_runtime_directories(borgmatic_runtime_directory):
'''
Return a list of borgmatic-specific source directories used for state like database backups.
Return a list of borgmatic-specific runtime directories used for temporary runtime data like
streaming database dumps and bootstrap metadata. If no such directories exist, return an empty
list.
'''
if not borgmatic_source_directory:
borgmatic_source_directory = state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
return (
[borgmatic_source_directory]
if os.path.exists(os.path.expanduser(borgmatic_source_directory))
else []
)
return [borgmatic_runtime_directory] if os.path.exists(borgmatic_runtime_directory) else []
ROOT_PATTERN_PREFIX = 'R '
@ -342,7 +337,7 @@ def make_base_create_command(
config_paths,
local_borg_version,
global_arguments,
borgmatic_source_directories,
borgmatic_runtime_directories,
local_path='borg',
remote_path=None,
progress=False,
@ -368,7 +363,7 @@ def make_base_create_command(
map_directories_to_devices(
expand_directories(
tuple(config.get('source_directories', ()))
+ borgmatic_source_directories
+ borgmatic_runtime_directories
+ tuple(config_paths if config.get('store_config_files', True) else ()),
working_directory=working_directory,
)
@ -479,7 +474,7 @@ def make_base_create_command(
local_path,
working_directory,
borg_environment,
skip_directories=borgmatic_source_directories,
skip_directories=borgmatic_runtime_directories,
)
if special_file_paths:
@ -528,8 +523,10 @@ def create_archive(
borgmatic.logger.add_custom_log_levels()
working_directory = borgmatic.config.paths.get_working_directory(config)
borgmatic_source_directories = expand_directories(
collect_borgmatic_source_directories(config.get('borgmatic_source_directory')),
borgmatic_runtime_directories = expand_directories(
collect_borgmatic_runtime_directories(
borgmatic.config.paths.get_borgmatic_runtime_directory(config)
),
working_directory=working_directory,
)
@ -541,7 +538,7 @@ def create_archive(
config_paths,
local_borg_version,
global_arguments,
borgmatic_source_directories,
borgmatic_runtime_directories,
local_path,
remote_path,
progress,

View File

@ -132,10 +132,9 @@ def extract_archive(
+ (('--progress',) if progress else ())
+ (('--stdout',) if extract_to_stdout else ())
+ flags.make_repository_archive_flags(
# Make the repository path absolute so the destination directory
# used below via changing the working directory doesn't prevent
# Borg from finding the repo. But also apply the user's configured
# working directory (if any) to the repo path.
# Make the repository path absolute so the destination directory used below via changing
# the working directory doesn't prevent Borg from finding the repo. But also apply the
# user's configured working directory (if any) to the repo path.
borgmatic.config.validate.normalize_repository_path(
os.path.join(working_directory or '', repository)
),

View File

@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
def local_borg_version(config, local_path='borg'):
'''
Given a configuration dict and a local Borg binary path, return a version string for it.
Given a configuration dict and a local Borg executable path, return a version string for it.
Raise OSError or CalledProcessError if there is a problem running Borg.
Raise ValueError if the version cannot be parsed.

View File

@ -74,11 +74,11 @@ def omit_values_colliding_with_action_names(unparsed_arguments, parsed_arguments
for action_name, parsed in parsed_arguments.items():
for value in vars(parsed).values():
if isinstance(value, str):
if value in ACTION_ALIASES.keys():
if value in ACTION_ALIASES.keys() and value in remaining_arguments:
remaining_arguments.remove(value)
elif isinstance(value, list):
for item in value:
if item in ACTION_ALIASES.keys():
if item in ACTION_ALIASES.keys() and item in remaining_arguments:
remaining_arguments.remove(item)
return tuple(remaining_arguments)
@ -864,9 +864,23 @@ def make_parsers():
help='Path of repository to extract config files from, quoted globs supported',
required=True,
)
config_bootstrap_group.add_argument(
'--local-path',
help='Alternate Borg local executable. Defaults to "borg"',
default='borg',
)
config_bootstrap_group.add_argument(
'--remote-path',
help='Alternate Borg remote executable. Defaults to "borg"',
default='borg',
)
config_bootstrap_group.add_argument(
'--user-runtime-directory',
help='Path used for temporary runtime data like bootstrap metadata. Defaults to $XDG_RUNTIME_DIR or /var/run/$UID',
)
config_bootstrap_group.add_argument(
'--borgmatic-source-directory',
help='Path that stores the config files used to create an archive and additional source files used for temporary internal state like borgmatic database dumps. Defaults to ~/.borgmatic',
help='Deprecated. Path formerly used for temporary runtime data like bootstrap metadata. Defaults to ~/.borgmatic',
)
config_bootstrap_group.add_argument(
'--archive',

View File

@ -694,7 +694,9 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
if 'bootstrap' in arguments:
try:
# No configuration file is needed for bootstrap.
local_borg_version = borg_version.local_borg_version({}, 'borg')
local_borg_version = borg_version.local_borg_version(
{}, arguments['bootstrap'].local_path
)
except (OSError, CalledProcessError, ValueError) as error:
yield from log_error_records('Error getting local Borg version', error)
return

View File

@ -29,18 +29,21 @@ def get_borgmatic_source_directory(config):
def get_borgmatic_runtime_directory(config):
'''
Given a configuration dict, get the borgmatic runtime directory used for storing temporary
runtime data like streaming database dumps and bootstrap metadata. Defaults to the
"borgmatic_source_directory" value (deprecated) or $XDG_RUNTIME_DIR/borgmatic or
/var/run/$UID/borgmatic.
runtime data like streaming database dumps and bootstrap metadata. Defaults to
$XDG_RUNTIME_DIR/./borgmatic or /run/user/$UID/./borgmatic.
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
does not get stored in the file path within an archive. That way, the path of the runtime
directory can change without leaving database dumps within an archive inaccessible.
'''
return expand_user_in_path(
config.get('borgmatic_runtime_directory')
or config.get('borgmatic_source_directory')
or os.path.join(
os.environ.get(
os.path.join(
config.get('user_runtime_directory')
or os.environ.get(
'XDG_RUNTIME_DIR',
f'/var/run/{os.getuid()}',
f'/run/user/{os.getuid()}',
),
'.',
'borgmatic',
)
)
@ -49,14 +52,13 @@ def get_borgmatic_runtime_directory(config):
def get_borgmatic_state_directory(config):
'''
Given a configuration dict, get the borgmatic state directory used for storing borgmatic state
files like records of when checks last ran. Defaults to the "borgmatic_source_directory" value
(deprecated) or $XDG_STATE_HOME/borgmatic or ~/.local/state/borgmatic.
files like records of when checks last ran. Defaults to $XDG_STATE_HOME/borgmatic or
~/.local/state/./borgmatic.
'''
return expand_user_in_path(
config.get('borgmatic_state_directory')
or config.get('borgmatic_source_directory')
or os.path.join(
os.environ.get(
os.path.join(
config.get('user_state_directory')
or os.environ.get(
'XDG_STATE_HOME',
'~/.local/state',
),

View File

@ -207,24 +207,27 @@ properties:
borgmatic_source_directory:
type: string
description: |
Deprecated. Replaced by borgmatic_runtime_directory and
Deprecated. Only used for locating database dumps and bootstrap
metadata within backup archives created prior to deprecation.
Replaced by borgmatic_runtime_directory and
borgmatic_state_directory. Defaults to ~/.borgmatic
example: /tmp/borgmatic
borgmatic_runtime_directory:
user_runtime_directory:
type: string
description: |
Path for storing temporary runtime data like streaming database
dumps and bootstrap metadata. Defaults to the
borgmatic_source_directory value (deprecated) or
$XDG_RUNTIME_DIR/borgmatic or /var/run/$UID/borgmatic.
dumps and bootstrap metadata. borgmatic automatically creates and
uses a "borgmatic" subdirectory here. Defaults to $XDG_RUNTIME_DIR
or /run/user/$UID.
example: /run/user/1001/borgmatic
borgmatic_state_directory:
user_state_directory:
type: string
description: |
Path for storing borgmatic state files like records of when checks
last ran. Defaults to the borgmatic_source_directory value
(deprecated) or $XDG_STATE_HOME/borgmatic or
~/.local/state/borgmatic.
last ran. borgmatic automatically creates and uses a "borgmatic"
subdirectory here. If you change this option, borgmatic must
create the check records again (and therefore re-run checks).
Defaults to $XDG_STATE_HOME or ~/.local/state.
example: /var/lib/borgmatic
store_config_files:
type: boolean

View File

@ -1,9 +1,8 @@
import fnmatch
import logging
import os
import shutil
from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
logger = logging.getLogger(__name__)
DATA_SOURCE_HOOK_NAMES = (
@ -15,15 +14,12 @@ DATA_SOURCE_HOOK_NAMES = (
)
def make_data_source_dump_path(borgmatic_source_directory, data_source_hook_name):
def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_name):
'''
Given a borgmatic source directory (or None) and a data source hook name, construct a data
source dump path.
Given a borgmatic runtime directory and a data source hook name, construct a data source dump
path.
'''
if not borgmatic_source_directory:
borgmatic_source_directory = DEFAULT_BORGMATIC_SOURCE_DIRECTORY
return os.path.join(borgmatic_source_directory, data_source_hook_name)
return os.path.join(borgmatic_runtime_directory, data_source_hook_name)
def make_data_source_dump_filename(dump_path, name, hostname=None):
@ -36,7 +32,7 @@ def make_data_source_dump_filename(dump_path, name, hostname=None):
if os.path.sep in name:
raise ValueError(f'Invalid data source name {name}')
return os.path.join(os.path.expanduser(dump_path), hostname or 'localhost', name)
return os.path.join(dump_path, hostname or 'localhost', name)
def create_parent_directory_for_dump(dump_path):
@ -63,18 +59,22 @@ def remove_data_source_dumps(dump_path, data_source_type_name, log_prefix, dry_r
logger.debug(f'{log_prefix}: Removing {data_source_type_name} data source dumps{dry_run_label}')
expanded_path = os.path.expanduser(dump_path)
if dry_run:
return
if os.path.exists(expanded_path):
shutil.rmtree(expanded_path)
if os.path.exists(dump_path):
shutil.rmtree(dump_path)
def convert_glob_patterns_to_borg_patterns(patterns):
def convert_glob_patterns_to_borg_pattern(patterns):
'''
Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
patterns like "sh:etc/*".
Convert a sequence of shell glob patterns like "/etc/*", "/tmp/*" to the corresponding Borg
regular expression archive pattern as a single string like "re:etc/.*|tmp/.*".
'''
return [f'sh:{pattern.lstrip(os.path.sep)}' for pattern in patterns]
# Remove the "\Z" generated by fnmatch.translate() because we don't want the pattern to match
# only at the end of a path, as directory format dumps require extracting files with paths
# longer than the pattern. E.g., a pattern of "borgmatic/*/foo_databases/test" should also match
# paths like "borgmatic/*/foo_databases/test/toc.dat"
return 're:' + '|'.join(
fnmatch.translate(pattern.lstrip('/')).replace('\\Z', '') for pattern in patterns
)

View File

@ -3,6 +3,7 @@ import logging
import os
import shlex
import borgmatic.config.paths
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@ -13,12 +14,14 @@ from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(config, base_directory=None): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a configuration dict and an optional base directory, make the corresponding dump path. If
a base directory isn't provided, use the borgmatic runtime directory.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mariadb_databases'
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
'mariadb_databases',
)
@ -191,13 +194,23 @@ def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma:
dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
'''
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
database name to match, return the corresponding glob patterns to match the database dump in an
archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path(config, 'borgmatic'), name, hostname='*'
),
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(

View File

@ -1,18 +1,21 @@
import logging
import shlex
import borgmatic.config.paths
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(config, base_directory=None): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a configuration dict and an optional base directory, make the corresponding dump path. If
a base directory isn't provided, use the borgmatic runtime directory.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mongodb_databases'
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
'mongodb_databases',
)
@ -100,13 +103,23 @@ def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma:
dump.remove_data_source_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
'''
Given a sequence of database configurations dicts, a configuration dict, a prefix to log with,
and a database name to match, return the corresponding glob patterns to match the database dump
in an archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path(config, 'borgmatic'), name, hostname='*'
),
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(

View File

@ -3,6 +3,7 @@ import logging
import os
import shlex
import borgmatic.config.paths
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@ -13,12 +14,14 @@ from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(config, base_directory=None): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a configuration dict and an optional base directory, make the corresponding dump path. If
a base directory isn't provided, use the borgmatic runtime directory.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'mysql_databases'
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
'mysql_databases',
)
@ -189,13 +192,23 @@ def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma:
dump.remove_data_source_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
'''
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
database name to match, return the corresponding glob patterns to match the database dump in an
archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path(config, 'borgmatic'), name, hostname='*'
),
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(

View File

@ -2,8 +2,10 @@ import csv
import itertools
import logging
import os
import pathlib
import shlex
import borgmatic.config.paths
from borgmatic.execute import (
execute_command,
execute_command_and_capture_output,
@ -14,12 +16,14 @@ from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(config, base_directory=None): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a configuration dict and an optional base directory, make the corresponding dump path. If
a base directory isn't provided, use the borgmatic runtime directory.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'postgresql_databases'
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
'postgresql_databases',
)
@ -215,13 +219,23 @@ def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma:
dump.remove_data_source_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
'''
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
database name to match, return the corresponding glob patterns to match the database dump in an
archive.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path(config, 'borgmatic'), name, hostname='*'
),
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(
@ -291,7 +305,7 @@ def restore_data_source_dump(
if 'restore_options' in data_source
else ()
)
+ (() if extract_process else (dump_filename,))
+ (() if extract_process else (str(pathlib.Path(dump_filename)),))
+ tuple(
itertools.chain.from_iterable(('--schema', schema) for schema in data_source['schemas'])
if data_source.get('schemas')

View File

@ -2,18 +2,21 @@ import logging
import os
import shlex
import borgmatic.config.paths
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(config): # pragma: no cover
def make_dump_path(config, base_directory=None): # pragma: no cover
'''
Make the dump path from the given configuration dict and the name of this hook.
Given a configuration dict and an optional base directory, make the corresponding dump path. If
a base directory isn't provided, use the borgmatic runtime directory.
'''
return dump.make_data_source_dump_path(
config.get('borgmatic_source_directory'), 'sqlite_databases'
base_directory or borgmatic.config.paths.get_borgmatic_runtime_directory(config),
'sqlite_databases',
)
@ -87,12 +90,22 @@ def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma:
dump.remove_data_source_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run)
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
def make_data_source_dump_patterns(databases, config, log_prefix, name=None): # pragma: no cover
'''
Make a pattern that matches the given SQLite databases. The databases are supplied as a sequence
of configuration dicts, as per the configuration schema.
'''
return dump.make_data_source_dump_filename(make_dump_path(config), name)
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path(config, 'borgmatic'), name, hostname='*'
),
dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*'),
dump.make_data_source_dump_filename(
make_dump_path(config, borgmatic_source_directory), name, hostname='*'
),
)
def restore_data_source_dump(

View File

@ -63,14 +63,23 @@ dump formats, which can't stream and therefore do consume temporary disk
space. Additionally, prior to borgmatic 1.5.3, all database dumps consumed
temporary disk space.)
To support this, borgmatic creates temporary named pipes in `~/.borgmatic` by
default. To customize this path, set the `borgmatic_source_directory` option
in borgmatic's configuration.
<span class="minilink minilink-addedin">New in version 1.9.0</span> To support
this, borgmatic creates temporary streaming database dumps within
`/run/user/$UID/borgmatic` by default (where `$UID` is the current user's ID).
To customize the `/run/user/$UID` portion of this path, set the
`user_runtime_directory` option in borgmatic's configuration. Alternatively,
set the `XDG_RUNTIME_DIR` environment variable (often already set to
`/run/user/$UID`).
Also note that using a database hook implicitly enables both the
`read_special` and `one_file_system` configuration settings (even if they're
disabled in your configuration) to support this dump and restore streaming.
See Limitations below for more on this.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
directory by default. At that time, the path was configurable by the
`borgmatic_source_directory` configuration option (now deprecated).
Also note that using a database hook implicitly enables the
`read_special` configuration option (even if it's disabled in your
configuration) to support this dump and restore streaming. See Limitations
below for more on this.
Here's a more involved example that connects to remote databases:

View File

@ -225,15 +225,26 @@ repository, at most once a month.
Unlike a real scheduler like cron, borgmatic only makes a best effort to run
checks on the configured frequency. It compares that frequency with how long
it's been since the last check for a given repository (as recorded in a file
within `~/.borgmatic/checks`). If it hasn't been long enough, the check is
skipped. And you still have to run `borgmatic check` (or `borgmatic` without
actions) in order for checks to run, even when a `frequency` is configured!
it's been since the last check for a given repository If it hasn't been long
enough, the check is skipped. And you still have to run `borgmatic check` (or
`borgmatic` without actions) in order for checks to run, even when a
`frequency` is configured!
This also applies *across* configuration files that have the same repository
configured. Make sure you have the same check frequency configured in each
though—or the most frequently configured check will apply.
<span class="minilink minilink-addedin">New in version 1.9.0</span>To support
this frequency logic, borgmatic records check timestamps within the
`~/.local/state/borgmatic/checks` directory. To override the `~/.local/state`
portion of this path, set the `user_state_directory` configuration option.
Alternatively, set the `XDG_STATE_HOME` environment variable.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
borgmatic recorded check timestamps within the `~/.borgmatic` directory. At
that time, the path was configurable by the `borgmatic_source_directory`
configuration option (now deprecated).
If you want to temporarily ignore your configured frequencies, you can invoke
`borgmatic check --force` to run checks unconditionally.

View File

@ -62,9 +62,9 @@ for available values.
(No borgmatic `list` or `info` actions? Upgrade borgmatic!)
<span class="minilink minilink-addedin">New in borgmatic version 1.9.0</span>
There are also `repo-list` and `repo-info` actions for displaying repository
information with Borg 2.x:
<span class="minilink minilink-addedin">New in version 1.9.0</span> There are
also `repo-list` and `repo-info` actions for displaying repository information
with Borg 2.x:
```bash
borgmatic repo-list
@ -107,12 +107,28 @@ hooks](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/), you
can list backed up database dumps via borgmatic. For example:
```bash
borgmatic list --archive latest --find .borgmatic/*_databases
borgmatic list --archive latest --find *borgmatic/*_databases
```
This gives you a listing of all database dump files contained in the latest
archive, complete with file sizes.
<span class="minilink minilink-addedin">New in borgmatic version
1.9.0</span>Database dump files are stored at `/borgmatic` within a backup
archive, regardless of the user who performs the backup. (Note that Borg
doesn't store the leading `/`.)
<span class="minilink minilink-addedin">With Borg version 1.2 and
earlier</span>Database dump files are stored at `/var/run/$UID/borgmatic`
(where `$UID` is the current user's ID) unless overridden by the
`user_runtime_directory` configuration option or the `XDG_STATE_HOME`
environment variable.
<span class="minilink minilink-addedin">Prior to borgmatic version
1.9.0</span>Database dump files were instead stored at `~/.borgmatic` within
the backup archive (where `~` was expanded to the home directory of the user
who performed the backup). This applied with all versions of Borg.
## Logging

View File

@ -22,7 +22,7 @@ following, all based on your borgmatic configuration files or command-line
arguments:
* configured repositories, running your Borg command once for each one
* local and remote Borg binary paths
* local and remote Borg executable paths
* SSH settings and Borg environment variables
* lock wait settings
* verbosity

View File

@ -14,7 +14,7 @@ def write_configuration(
source_directory,
config_path,
repository_path,
borgmatic_source_directory,
user_runtime_directory,
postgresql_dump_format='custom',
mongodb_dump_format='archive',
):
@ -28,7 +28,7 @@ source_directories:
- {source_directory}
repositories:
- path: {repository_path}
borgmatic_source_directory: {borgmatic_source_directory}
user_runtime_directory: {user_runtime_directory}
encryption_passphrase: "test"
@ -101,7 +101,7 @@ def write_custom_restore_configuration(
source_directory,
config_path,
repository_path,
borgmatic_source_directory,
user_runtime_directory,
postgresql_dump_format='custom',
mongodb_dump_format='archive',
):
@ -115,7 +115,7 @@ source_directories:
- {source_directory}
repositories:
- path: {repository_path}
borgmatic_source_directory: {borgmatic_source_directory}
user_runtime_directory: {user_runtime_directory}
encryption_passphrase: "test"
@ -173,7 +173,7 @@ def write_simple_custom_restore_configuration(
source_directory,
config_path,
repository_path,
borgmatic_source_directory,
user_runtime_directory,
postgresql_dump_format='custom',
):
'''
@ -187,7 +187,7 @@ source_directories:
- {source_directory}
repositories:
- path: {repository_path}
borgmatic_source_directory: {borgmatic_source_directory}
user_runtime_directory: {user_runtime_directory}
encryption_passphrase: "test"
@ -347,7 +347,6 @@ def test_database_dump_and_restore():
# Create a Borg repository.
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
borgmatic_source_directory = os.path.join(temporary_directory, '.borgmatic')
# Write out a special file to ensure that it gets properly excluded and Borg doesn't hang on it.