From dd16504329489c0921427f3ae414231039b08bd7 Mon Sep 17 00:00:00 2001
From: Matthew Daley
Date: Fri, 13 Dec 2019 15:42:46 +1300
Subject: [PATCH 01/39] Use --remote-path, --debug and --info when checking for
repo existence
These are currently not being used in the call to `borg info` performed
as part of the borgmatic init command to check whether or not the repo
already exists.
---
borgmatic/borg/init.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/borgmatic/borg/init.py b/borgmatic/borg/init.py
index 152f8c1b..08256aef 100644
--- a/borgmatic/borg/init.py
+++ b/borgmatic/borg/init.py
@@ -23,7 +23,13 @@ def initialize_repository(
whether the repository should be append-only, and the storage quota to use, initialize the
repository. If the repository already exists, then log and skip initialization.
'''
- info_command = (local_path, 'info', repository)
+ info_command = (
+ (local_path, 'info')
+ + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ + (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
+ + (('--remote-path', remote_path) if remote_path else ())
+ + (repository,)
+ )
logger.debug(' '.join(info_command))
try:
From f1358d52aaa9e6926636a8660f57669fa2abe8ae Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Thu, 12 Dec 2019 21:50:24 -0800
Subject: [PATCH 02/39] Add "borgmatic init" repository probing fix to NEWS.
---
NEWS | 3 +++
setup.py | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/NEWS b/NEWS
index a0eac709..49a167f9 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+1.4.20
+ * Fix repository probing during "borgmatic init" to respect verbosity flag and remote_path option.
+
1.4.19
* #259: Optionally change the internal database dump path via "borgmatic_source_directory" option
in location configuration section.
diff --git a/setup.py b/setup.py
index 18c8ff87..e61d3ac8 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
-VERSION = '1.4.19'
+VERSION = '1.4.20'
setup(
From e009bfeaa20d173ebe189cecb47c43819f747aa0 Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Thu, 12 Dec 2019 22:54:45 -0800
Subject: [PATCH 03/39] Update Healthchecks/Cronitor/Cronhub monitoring
integrations to fire for "check" and "prune" actions, not just "create"
(#249).
---
NEWS | 2 +
borgmatic/commands/borgmatic.py | 65 ++++++++++++++-------------
docs/how-to/monitor-your-backups.md | 31 +++++++------
tests/unit/commands/test_borgmatic.py | 35 ++++++++++++++-
4 files changed, 87 insertions(+), 46 deletions(-)
diff --git a/NEWS b/NEWS
index 49a167f9..61abaf45 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,7 @@
1.4.20
* Fix repository probing during "borgmatic init" to respect verbosity flag and remote_path option.
+ * #249: Update Healthchecks/Cronitor/Cronhub monitoring integrations to fire for "check" and
+ "prune" actions, not just "create".
1.4.19
* #259: Optionally change the internal database dump path via "borgmatic_source_directory" option
diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py
index 3b539764..f3da1618 100644
--- a/borgmatic/commands/borgmatic.py
+++ b/borgmatic/commands/borgmatic.py
@@ -53,8 +53,8 @@ def run_configuration(config_filename, config, arguments):
encountered_error = None
error_repository = ''
- if 'create' in arguments:
- try:
+ try:
+ if {'prune', 'create', 'check'}.intersection(arguments):
dispatch.call_hooks(
'ping_monitor',
hooks,
@@ -63,6 +63,7 @@ def run_configuration(config_filename, config, arguments):
monitor.State.START,
global_arguments.dry_run,
)
+ if 'create' in arguments:
command.execute_hook(
hooks.get('before_backup'),
hooks.get('umask'),
@@ -78,11 +79,11 @@ def run_configuration(config_filename, config, arguments):
location,
global_arguments.dry_run,
)
- except (OSError, CalledProcessError) as error:
- encountered_error = error
- yield from make_error_log_records(
- '{}: Error running pre-backup hook'.format(config_filename), error
- )
+ except (OSError, CalledProcessError) as error:
+ encountered_error = error
+ yield from make_error_log_records(
+ '{}: Error running pre-backup hook'.format(config_filename), error
+ )
if not encountered_error:
for repository_path in location['repositories']:
@@ -105,31 +106,33 @@ def run_configuration(config_filename, config, arguments):
'{}: Error running actions for repository'.format(repository_path), error
)
- if 'create' in arguments and not encountered_error:
+ if not encountered_error:
try:
- dispatch.call_hooks(
- 'remove_database_dumps',
- hooks,
- config_filename,
- dump.DATABASE_HOOK_NAMES,
- location,
- global_arguments.dry_run,
- )
- command.execute_hook(
- hooks.get('after_backup'),
- hooks.get('umask'),
- config_filename,
- 'post-backup',
- global_arguments.dry_run,
- )
- dispatch.call_hooks(
- 'ping_monitor',
- hooks,
- config_filename,
- monitor.MONITOR_HOOK_NAMES,
- monitor.State.FINISH,
- global_arguments.dry_run,
- )
+ if 'create' in arguments:
+ dispatch.call_hooks(
+ 'remove_database_dumps',
+ hooks,
+ config_filename,
+ dump.DATABASE_HOOK_NAMES,
+ location,
+ global_arguments.dry_run,
+ )
+ command.execute_hook(
+ hooks.get('after_backup'),
+ hooks.get('umask'),
+ config_filename,
+ 'post-backup',
+ global_arguments.dry_run,
+ )
+ if {'prune', 'create', 'check'}.intersection(arguments):
+ dispatch.call_hooks(
+ 'ping_monitor',
+ hooks,
+ config_filename,
+ monitor.MONITOR_HOOK_NAMES,
+ monitor.State.FINISH,
+ global_arguments.dry_run,
+ )
except (OSError, CalledProcessError) as error:
encountered_error = error
yield from make_error_log_records(
diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md
index cddadb01..f2f105da 100644
--- a/docs/how-to/monitor-your-backups.md
+++ b/docs/how-to/monitor-your-backups.md
@@ -116,21 +116,22 @@ hooks:
With this hook in place, borgmatic pings your Healthchecks project when a
backup begins, ends, or errors. Specifically, before the `before_backup`
-hooks run, borgmatic lets Healthchecks know that a backup has started.
+hooks run, borgmatic lets Healthchecks know that it has started if any of
+the `prune`, `create`, or `check` actions are run.
-Then, if the backup completes successfully, borgmatic notifies Healthchecks of
+Then, if the actions complete successfully, borgmatic notifies Healthchecks of
the success after the `after_backup` hooks run, and includes borgmatic logs in
the payload data sent to Healthchecks. This means that borgmatic logs show up
in the Healthchecks UI, although be aware that Healthchecks currently has a
10-kilobyte limit for the logs in each ping.
-If an error occurs during the backup, borgmatic notifies Healthchecks after
+If an error occurs during any action, borgmatic notifies Healthchecks after
the `on_error` hooks run, also tacking on logs including the error itself. But
-the logs are only included for errors that occur within the borgmatic `create`
-action (and not other actions).
+the logs are only included for errors that occur when a `prune`, `create`, or
+`check` action is run.
Note that borgmatic sends logs to Healthchecks by applying the maximum of any
-other borgmatic verbosity level (`--verbosity`, `--syslog-verbosity`, etc.),
+other borgmatic verbosity levels (`--verbosity`, `--syslog-verbosity`, etc.),
as there is not currently a dedicated Healthchecks verbosity setting.
You can configure Healthchecks to notify you by a [variety of
@@ -155,10 +156,11 @@ hooks:
With this hook in place, borgmatic pings your Cronitor monitor when a backup
begins, ends, or errors. Specifically, before the `before_backup`
-hooks run, borgmatic lets Cronitor know that a backup has started. Then,
-if the backup completes successfully, borgmatic notifies Cronitor of the
-success after the `after_backup` hooks run. And if an error occurs during the
-backup, borgmatic notifies Cronitor after the `on_error` hooks run.
+hooks run, borgmatic lets Cronitor know that it has started if any of the
+`prune`, `create`, or `check` actions are run. Then, if the actions complete
+successfully, borgmatic notifies Cronitor of the success after the
+`after_backup` hooks run. And if an error occurs during any action, borgmatic
+notifies Cronitor after the `on_error` hooks run.
You can configure Cronitor to notify you by a [variety of
mechanisms](https://cronitor.io/docs/cron-job-notifications) when backups fail
@@ -182,10 +184,11 @@ hooks:
With this hook in place, borgmatic pings your Cronhub monitor when a backup
begins, ends, or errors. Specifically, before the `before_backup`
-hooks run, borgmatic lets Cronhub know that a backup has started. Then,
-if the backup completes successfully, borgmatic notifies Cronhub of the
-success after the `after_backup` hooks run. And if an error occurs during the
-backup, borgmatic notifies Cronhub after the `on_error` hooks run.
+hooks run, borgmatic lets Cronhub know that it has started if any of the
+`prune`, `create`, or `check` actions are run. Then, if the actions complete
+successfully, borgmatic notifies Cronhub of the success after the
+`after_backup` hooks run. And if an error occurs during any action, borgmatic
+notifies Cronhub after the `on_error` hooks run.
Note that even though you configure borgmatic with the "start" variant of the
ping URL, borgmatic substitutes the correct state into the URL when pinging
diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py
index 1bdb3213..a2b98f37 100644
--- a/tests/unit/commands/test_borgmatic.py
+++ b/tests/unit/commands/test_borgmatic.py
@@ -20,7 +20,18 @@ def test_run_configuration_runs_actions_for_each_repository():
assert results == expected_results
-def test_run_configuration_executes_hooks_for_create_action():
+def test_run_configuration_calls_hooks_for_prune_action():
+ flexmock(module.borg_environment).should_receive('initialize')
+ flexmock(module.command).should_receive('execute_hook').never()
+ flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
+ flexmock(module).should_receive('run_actions').and_return([])
+ config = {'location': {'repositories': ['foo']}}
+ arguments = {'global': flexmock(dry_run=False), 'prune': flexmock()}
+
+ list(module.run_configuration('test.yaml', config, arguments))
+
+
+def test_run_configuration_executes_and_calls_hooks_for_create_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
@@ -31,6 +42,28 @@ def test_run_configuration_executes_hooks_for_create_action():
list(module.run_configuration('test.yaml', config, arguments))
+def test_run_configuration_calls_hooks_for_check_action():
+ flexmock(module.borg_environment).should_receive('initialize')
+ flexmock(module.command).should_receive('execute_hook').never()
+ flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
+ flexmock(module).should_receive('run_actions').and_return([])
+ config = {'location': {'repositories': ['foo']}}
+ arguments = {'global': flexmock(dry_run=False), 'check': flexmock()}
+
+ list(module.run_configuration('test.yaml', config, arguments))
+
+
+def test_run_configuration_does_not_trigger_hooks_for_list_action():
+ flexmock(module.borg_environment).should_receive('initialize')
+ flexmock(module.command).should_receive('execute_hook').never()
+ flexmock(module.dispatch).should_receive('call_hooks').never()
+ flexmock(module).should_receive('run_actions').and_return([])
+ config = {'location': {'repositories': ['foo']}}
+ arguments = {'global': flexmock(dry_run=False), 'list': flexmock()}
+
+ list(module.run_configuration('test.yaml', config, arguments))
+
+
def test_run_configuration_logs_actions_error():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.command).should_receive('execute_hook')
From afaabd14a862764a7e8d57377deb3eb7c02799ba Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Fri, 13 Dec 2019 11:42:17 -0800
Subject: [PATCH 04/39] Clarify documentation on how /etc/borgmatic.d/
configuration files are interpreted.
---
docs/how-to/make-per-application-backups.md | 5 +++++
docs/how-to/set-up-backups.md | 12 ++++--------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/docs/how-to/make-per-application-backups.md b/docs/how-to/make-per-application-backups.md
index f962b813..840e2a6d 100644
--- a/docs/how-to/make-per-application-backups.md
+++ b/docs/how-to/make-per-application-backups.md
@@ -22,6 +22,11 @@ When you set up multiple configuration files like this, borgmatic will run
each one in turn from a single borgmatic invocation. This includes, by
default, the traditional `/etc/borgmatic/config.yaml` as well.
+Each configuration file is interpreted independently, as if you ran borgmatic
+for each configuration file one at a time. In other words, borgmatic does not
+perform any merging of configuration files by default. If you'd like borgmatic
+to merge your configuration files, see below about configuration includes.
+
And if you need even more customizability, you can specify alternate
configuration paths on the command-line with borgmatic's `--config` option.
See `borgmatic --help` for more information.
diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md
index 2a6704dd..bc78237c 100644
--- a/docs/how-to/set-up-backups.md
+++ b/docs/how-to/set-up-backups.md
@@ -3,15 +3,11 @@ title: How to set up backups with borgmatic
---
## Installation
-To get up and running, first [install
-Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at
-least version 1.1.
+First, [install
+Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
+version 1.1.
-By default, borgmatic looks for its configuration files in `/etc/borgmatic/`
-and `/etc/borgmatic.d/`, where the root user typically has read access.
-
-So, to download and install borgmatic as the root user, run the following
-commands:
+Then, download and install borgmatic by running the following command:
```bash
sudo pip3 install --user --upgrade borgmatic
From f787dfe809561cb6d27005b43b9d58bd958bb818 Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Tue, 17 Dec 2019 11:46:27 -0800
Subject: [PATCH 05/39] Override particular configuration options from the
command-line via "--override" flag (#268).
---
NEWS | 5 ++
borgmatic/commands/arguments.py | 7 ++
borgmatic/commands/borgmatic.py | 6 +-
borgmatic/config/override.py | 71 ++++++++++++++++++
borgmatic/config/validate.py | 13 ++--
docs/how-to/make-per-application-backups.md | 34 +++++++++
setup.py | 2 +-
tests/integration/config/test_override.py | 40 ++++++++++
tests/integration/config/test_validate.py | 27 +++++++
tests/unit/config/test_override.py | 82 +++++++++++++++++++++
10 files changed, 278 insertions(+), 9 deletions(-)
create mode 100644 borgmatic/config/override.py
create mode 100644 tests/integration/config/test_override.py
create mode 100644 tests/unit/config/test_override.py
diff --git a/NEWS b/NEWS
index 61abaf45..dd1c9539 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,8 @@
+1.4.21.dev0
+ * #268: Override particular configuration options from the command-line via "--override" flag. See
+ the documentation for more information:
+ https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-overrides
+
1.4.20
* Fix repository probing during "borgmatic init" to respect verbosity flag and remote_path option.
* #249: Update Healthchecks/Cronitor/Cronhub monitoring integrations to fire for "check" and
diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py
index 738981ca..63030066 100644
--- a/borgmatic/commands/arguments.py
+++ b/borgmatic/commands/arguments.py
@@ -164,6 +164,13 @@ def parse_arguments(*unparsed_arguments):
default=None,
help='Write log messages to this file instead of syslog',
)
+ global_group.add_argument(
+ '--override',
+ metavar='SECTION.OPTION=VALUE',
+ nargs='+',
+ dest='overrides',
+ help='One or more configuration file options to override with specified values',
+ )
global_group.add_argument(
'--version',
dest='version',
diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py
index f3da1618..ce3719d8 100644
--- a/borgmatic/commands/borgmatic.py
+++ b/borgmatic/commands/borgmatic.py
@@ -372,7 +372,7 @@ def run_actions(
yield json.loads(json_output)
-def load_configurations(config_filenames):
+def load_configurations(config_filenames, overrides=None):
'''
Given a sequence of configuration filenames, load and validate each configuration file. Return
the results as a tuple of: dict of configuration filename to corresponding parsed configuration,
@@ -386,7 +386,7 @@ def load_configurations(config_filenames):
for config_filename in config_filenames:
try:
configs[config_filename] = validate.parse_configuration(
- config_filename, validate.schema_filename()
+ config_filename, validate.schema_filename(), overrides
)
except (ValueError, OSError, validate.Validation_error) as error:
logs.extend(
@@ -584,7 +584,7 @@ def main(): # pragma: no cover
sys.exit(0)
config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
- configs, parse_logs = load_configurations(config_filenames)
+ configs, parse_logs = load_configurations(config_filenames, global_arguments.overrides)
colorama.init(autoreset=True, strip=not should_do_markup(global_arguments.no_color, configs))
try:
diff --git a/borgmatic/config/override.py b/borgmatic/config/override.py
new file mode 100644
index 00000000..eb86077b
--- /dev/null
+++ b/borgmatic/config/override.py
@@ -0,0 +1,71 @@
+import io
+
+import ruamel.yaml
+
+
+def set_values(config, keys, value):
+ '''
+ Given a hierarchy of configuration dicts, a sequence of parsed key strings, and a string value,
+ descend into the hierarchy based on the keys to set the value into the right place.
+ '''
+ if not keys:
+ return
+
+ first_key = keys[0]
+ if len(keys) == 1:
+ config[first_key] = value
+ return
+
+ if first_key not in config:
+ config[first_key] = {}
+
+ set_values(config[first_key], keys[1:], value)
+
+
+def convert_value_type(value):
+ '''
+ Given a string value, determine its logical type (string, boolean, integer, etc.), and return it
+ converted to that type.
+ '''
+ return ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
+
+
+def parse_overrides(raw_overrides):
+ '''
+ Given a sequence of configuration file override strings in the form of "section.option=value",
+ parse and return a sequence of tuples (keys, values), where keys is a sequence of strings. For
+ instance, given the following raw overrides:
+
+ ['section.my_option=value1', 'section.other_option=value2']
+
+ ... return this:
+
+ (
+ (('section', 'my_option'), 'value1'),
+ (('section', 'other_option'), 'value2'),
+ )
+
+ Raise ValueError if an override can't be parsed.
+ '''
+ if not raw_overrides:
+ return ()
+
+ try:
+ return tuple(
+ (tuple(raw_keys.split('.')), convert_value_type(value))
+ for raw_override in raw_overrides
+ for raw_keys, value in (raw_override.split('=', 1),)
+ )
+ except ValueError:
+ raise ValueError('Invalid override. Make sure you use the form: SECTION.OPTION=VALUE')
+
+
+def apply_overrides(config, raw_overrides):
+ '''
+ Given a sequence of configuration file override strings in the form of "section.option=value"
+ and a configuration dict, parse each override and set it the configuration dict.
+ '''
+ overrides = parse_overrides(raw_overrides)
+
+ for (keys, value) in overrides:
+ set_values(config, keys, value)
diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py
index 1e421d18..b7d34a9f 100644
--- a/borgmatic/config/validate.py
+++ b/borgmatic/config/validate.py
@@ -6,7 +6,7 @@ import pykwalify.core
import pykwalify.errors
import ruamel.yaml
-from borgmatic.config import load
+from borgmatic.config import load, override
def schema_filename():
@@ -82,11 +82,12 @@ def remove_examples(schema):
return schema
-def parse_configuration(config_filename, schema_filename):
+def parse_configuration(config_filename, schema_filename, overrides=None):
'''
- Given the path to a config filename in YAML format and the path to a schema filename in
- pykwalify YAML schema format, return the parsed configuration as a data structure of nested
- dicts and lists corresponding to the schema. Example return value:
+ Given the path to a config filename in YAML format, the path to a schema filename in pykwalify
+ YAML schema format, a sequence of configuration file override strings in the form of
+ "section.option=value", return the parsed configuration as a data structure of nested dicts and
+ lists corresponding to the schema. Example return value:
{'location': {'source_directories': ['/home', '/etc'], 'repository': 'hostname.borg'},
'retention': {'keep_daily': 7}, 'consistency': {'checks': ['repository', 'archives']}}
@@ -102,6 +103,8 @@ def parse_configuration(config_filename, schema_filename):
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
raise Validation_error(config_filename, (str(error),))
+ override.apply_overrides(config, overrides)
+
validator = pykwalify.core.Core(source_data=config, schema_data=remove_examples(schema))
parsed_result = validator.validate(raise_exception=False)
diff --git a/docs/how-to/make-per-application-backups.md b/docs/how-to/make-per-application-backups.md
index 840e2a6d..cd257460 100644
--- a/docs/how-to/make-per-application-backups.md
+++ b/docs/how-to/make-per-application-backups.md
@@ -115,6 +115,40 @@ Note that this `<<` include merging syntax is only for merging in mappings
directly, please see the section above about standard includes.
+## Configuration overrides
+
+In more complex multi-application setups, you may want to override particular
+borgmatic configuration file options at the time you run borgmatic. For
+instance, you could reuse a common configuration file for multiple
+applications, but then set the repository for each application at runtime. Or
+you might want to try a variant of an option for testing purposes without
+actually touching your configuration file.
+
+Whatever the reason, you can override borgmatic configuration options at the
+command-line via the `--override` flag. Here's an example:
+
+```bash
+borgmatic create --override location.remote_path=borg1
+```
+
+What this does is load your configuration files, and for each one, disregard
+the configured value for the `remote_path` option in the `location` section,
+and use the value of `borg1` instead.
+
+Note that the value is parsed as an actual YAML string, so you can even set
+list values by using brackets. For instance:
+
+```bash
+borgmatic create --override location.repositories=[test1.borg,test2.borg]
+```
+
+There is not currently a way to override a single element of a list without
+replacing the whole list.
+
+Be sure to quote your overrides if they contain spaces or other characters
+that your shell may interpret.
+
+
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
diff --git a/setup.py b/setup.py
index e61d3ac8..0d87bc59 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
-VERSION = '1.4.20'
+VERSION = '1.4.21.dev0'
setup(
diff --git a/tests/integration/config/test_override.py b/tests/integration/config/test_override.py
new file mode 100644
index 00000000..cfcd3394
--- /dev/null
+++ b/tests/integration/config/test_override.py
@@ -0,0 +1,40 @@
+import pytest
+
+from borgmatic.config import override as module
+
+
+@pytest.mark.parametrize(
+ 'value,expected_result',
+ (
+ ('thing', 'thing'),
+ ('33', 33),
+ ('33b', '33b'),
+ ('true', True),
+ ('false', False),
+ ('[foo]', ['foo']),
+ ('[foo, bar]', ['foo', 'bar']),
+ ),
+)
+def test_convert_value_type_coerces_values(value, expected_result):
+ assert module.convert_value_type(value) == expected_result
+
+
+def test_apply_overrides_updates_config():
+ raw_overrides = [
+ 'section.key=value1',
+ 'other_section.thing=value2',
+ 'section.nested.key=value3',
+ 'new.foo=bar',
+ ]
+ config = {
+ 'section': {'key': 'value', 'other': 'other_value'},
+ 'other_section': {'thing': 'thing_value'},
+ }
+
+ module.apply_overrides(config, raw_overrides)
+
+ assert config == {
+ 'section': {'key': 'value1', 'other': 'other_value', 'nested': {'key': 'value3'}},
+ 'other_section': {'thing': 'value2'},
+ 'new': {'foo': 'bar'},
+ }
diff --git a/tests/integration/config/test_validate.py b/tests/integration/config/test_validate.py
index 706743bc..cbd4f5ba 100644
--- a/tests/integration/config/test_validate.py
+++ b/tests/integration/config/test_validate.py
@@ -212,3 +212,30 @@ def test_parse_configuration_raises_for_validation_error():
with pytest.raises(module.Validation_error):
module.parse_configuration('config.yaml', 'schema.yaml')
+
+
+def test_parse_configuration_applies_overrides():
+ mock_config_and_schema(
+ '''
+ location:
+ source_directories:
+ - /home
+
+ repositories:
+ - hostname.borg
+
+ local_path: borg1
+ '''
+ )
+
+ result = module.parse_configuration(
+ 'config.yaml', 'schema.yaml', overrides=['location.local_path=borg2']
+ )
+
+ assert result == {
+ 'location': {
+ 'source_directories': ['/home'],
+ 'repositories': ['hostname.borg'],
+ 'local_path': 'borg2',
+ }
+ }
diff --git a/tests/unit/config/test_override.py b/tests/unit/config/test_override.py
new file mode 100644
index 00000000..6925ca42
--- /dev/null
+++ b/tests/unit/config/test_override.py
@@ -0,0 +1,82 @@
+import pytest
+from flexmock import flexmock
+
+from borgmatic.config import override as module
+
+
+def test_set_values_with_empty_keys_bails():
+ config = {}
+
+ module.set_values(config, keys=(), value='value')
+
+ assert config == {}
+
+
+def test_set_values_with_one_key_sets_it_into_config():
+ config = {}
+
+ module.set_values(config, keys=('key',), value='value')
+
+ assert config == {'key': 'value'}
+
+
+def test_set_values_with_one_key_overwrites_existing_key():
+ config = {'key': 'old_value', 'other': 'other_value'}
+
+ module.set_values(config, keys=('key',), value='value')
+
+ assert config == {'key': 'value', 'other': 'other_value'}
+
+
+def test_set_values_with_multiple_keys_creates_hierarchy():
+ config = {}
+
+ module.set_values(config, ('section', 'key'), 'value')
+
+ assert config == {'section': {'key': 'value'}}
+
+
+def test_set_values_with_multiple_keys_updates_hierarchy():
+ config = {'section': {'other': 'other_value'}}
+ module.set_values(config, ('section', 'key'), 'value')
+
+ assert config == {'section': {'key': 'value', 'other': 'other_value'}}
+
+
+def test_parse_overrides_splits_keys_and_values():
+ flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
+ raw_overrides = ['section.my_option=value1', 'section.other_option=value2']
+ expected_result = (
+ (('section', 'my_option'), 'value1'),
+ (('section', 'other_option'), 'value2'),
+ )
+
+ module.parse_overrides(raw_overrides) == expected_result
+
+
+def test_parse_overrides_allows_value_with_equal_sign():
+ flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
+ raw_overrides = ['section.option=this===value']
+ expected_result = ((('section', 'option'), 'this===value'),)
+
+ module.parse_overrides(raw_overrides) == expected_result
+
+
+def test_parse_overrides_raises_on_missing_equal_sign():
+ flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
+ raw_overrides = ['section.option']
+
+ with pytest.raises(ValueError):
+ module.parse_overrides(raw_overrides)
+
+
+def test_parse_overrides_allows_value_with_single_key():
+ flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
+ raw_overrides = ['option=value']
+ expected_result = ((('option',), 'value'),)
+
+ module.parse_overrides(raw_overrides) == expected_result
+
+
+def test_parse_overrides_handles_empty_overrides():
+ module.parse_overrides(raw_overrides=None) == ()
From ed2ca9f47691702e7c8348cb009af907999e7308 Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Tue, 17 Dec 2019 20:06:25 -0800
Subject: [PATCH 06/39] Sign release files.
---
scripts/release | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/scripts/release b/scripts/release
index 0477b6ba..afc99c30 100755
--- a/scripts/release
+++ b/scripts/release
@@ -23,10 +23,11 @@ git push github $version
rm -fr dist
python3 setup.py bdist_wheel
python3 setup.py sdist
+gpg --detach-sign --armor dist/*
twine upload -r pypi dist/borgmatic-*.tar.gz
twine upload -r pypi dist/borgmatic-*-py3-none-any.whl
-# Set release changelogs on projects.evoworx.org and GitHub.
+# Set release changelogs on projects.torsion.org and GitHub.
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"
escaped_release_changelog="$(echo "$release_changelog" | sed -z 's/\n/\\n/g' | sed -z 's/\"/\\"/g')"
curl --silent --request POST \
From d64bcd5e83990e9682815ba8f4abad73c4ca564f Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Tue, 17 Dec 2019 20:12:41 -0800
Subject: [PATCH 07/39] When pruning with verbosity level 1, list pruned and
kept archives.
---
NEWS | 2 ++
borgmatic/borg/prune.py | 2 +-
tests/unit/borg/test_prune.py | 4 +++-
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/NEWS b/NEWS
index dd1c9539..f27b9b1e 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,8 @@
* #268: Override particular configuration options from the command-line via "--override" flag. See
the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-overrides
+ * When pruning with verbosity level 1, list pruned and kept archives. Previously, this information
+ was only shown at verbosity level 2.
1.4.20
* Fix repository probing during "borgmatic init" to respect verbosity flag and remote_path option.
diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py
index 0913cdeb..2c4811eb 100644
--- a/borgmatic/borg/prune.py
+++ b/borgmatic/borg/prune.py
@@ -58,7 +58,7 @@ def prune_archives(
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--stats',) if not dry_run and logger.isEnabledFor(logging.INFO) else ())
- + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ + (('--info', '--list') if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--stats',) if stats else ())
diff --git a/tests/unit/borg/test_prune.py b/tests/unit/borg/test_prune.py
index 80cce836..b2b4785a 100644
--- a/tests/unit/borg/test_prune.py
+++ b/tests/unit/borg/test_prune.py
@@ -75,7 +75,9 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
- insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO)
+ insert_execute_command_mock(
+ PRUNE_COMMAND + ('--stats', '--info', '--list', 'repo'), logging.INFO
+ )
insert_logging_mock(logging.INFO)
module.prune_archives(
From 6bfa0783b933cbb8282c219f84355c6b9c2285b0 Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Tue, 17 Dec 2019 20:16:13 -0800
Subject: [PATCH 08/39] Clarify that the documentation suggestion form is only
for documentation.
---
docs/_includes/components/suggestion-form.html | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/_includes/components/suggestion-form.html b/docs/_includes/components/suggestion-form.html
index c4e59b20..8e3a73a6 100644
--- a/docs/_includes/components/suggestion-form.html
+++ b/docs/_includes/components/suggestion-form.html
@@ -1,12 +1,12 @@
Improve this documentation
Have an idea on how to make this documentation even better? Send your
-feedback below! (But if you need help installing or using borgmatic, please
-use our issue tracker
-instead.)
+feedback below! But if you need help with borgmatic, or have an idea for a
+borgmatic feature, please use our issue
+tracker instead.