From cc9dbb1def6a289736d5608fdbdd967462e345ba Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Mon, 10 Dec 2018 22:20:57 -0800 Subject: [PATCH] Support for Borg repository initialization via borgmatic --init command-line flag (#110). --- NEWS | 6 +- README.md | 287 ++++++++++--------- borgmatic/borg/create.py | 14 - borgmatic/borg/environment.py | 15 + borgmatic/borg/init.py | 31 ++ borgmatic/commands/borgmatic.py | 52 +++- setup.py | 2 +- tests/end-to-end/test_borgmatic.py | 13 +- tests/integration/commands/test_borgmatic.py | 77 ++++- tests/unit/borg/test_create.py | 47 --- tests/unit/borg/test_environment.py | 49 ++++ tests/unit/borg/test_init.py | 58 ++++ tests/unit/test_verbosity.py | 6 +- 13 files changed, 440 insertions(+), 217 deletions(-) create mode 100644 borgmatic/borg/environment.py create mode 100644 borgmatic/borg/init.py create mode 100644 tests/unit/borg/test_environment.py create mode 100644 tests/unit/borg/test_init.py diff --git a/NEWS b/NEWS index 54cf6c6..50ed98f 100644 --- a/NEWS +++ b/NEWS @@ -1,7 +1,9 @@ -1.2.12.dev0 +1.2.12 + * #110: Support for Borg repository initialization via borgmatic --init command-line flag. * #111: Update Borg create --filter values so a dry run lists files to back up. * #113: Update README with link to a new/forked Docker image. - * Error when deprecated --excludes command-line option is used. + * Prevent deprecated --excludes command-line option from being used. + * Refactor README a bit to flow better for first-time users. 1.2.11 * #108: Support for Borg create --progress via borgmatic command-line flag. diff --git a/README.md b/README.md index 90c4d72..12961bb 100644 --- a/README.md +++ b/README.md @@ -58,28 +58,9 @@ href="https://asciinema.org/a/203761" target="_blank">screencast. To get up and running, first [install Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at -least version 1.1. Then, follow the [Borg Quick -Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) to create -a repository on a local or remote host. +least version 1.1. -Note that if you plan to run borgmatic on a schedule with cron, and you -encrypt your Borg repository with a passphrase instead of a key file, you'll -either need to set the borgmatic `encryption_passphrase` configuration -variable or set the `BORG_PASSPHRASE` environment variable. See the -[repository encryption -section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption) -of the Quick Start for more info. - -Alternatively, the passphrase can be specified programatically by setting -either the borgmatic `encryption_passcommand` configuration variable or the -`BORG_PASSCOMMAND` environment variable. See the [Borg Security -FAQ](http://borgbackup.readthedocs.io/en/stable/faq.html#how-can-i-specify-the-encryption-passphrase-programmatically) -for more info. - -If the repository is on a remote host, make sure that your local root user has -key-based ssh access to the desired user account on the remote host. - -To install borgmatic, run the following command to download and install it: +Then, run the following command to download and install borgmatic: ```bash sudo pip3 install --upgrade borgmatic @@ -88,6 +69,7 @@ sudo pip3 install --upgrade borgmatic Note that your pip binary may have a different name than "pip3". Make sure you're using Python 3, as borgmatic does not support Python 2. + ### Other ways to install * [A borgmatic Docker image](https://hub.docker.com/r/monachus/borgmatic/) based @@ -101,6 +83,7 @@ you're using Python 3, as borgmatic does not support Python 2. * [A borgmatic package for OpenBSD](http://ports.su/sysutils/borgmatic).

+ ## Configuration After you install borgmatic, generate a sample configuration file: @@ -124,6 +107,161 @@ borgmatic has added new options since you originally created your configuration file. +### Encryption + +Note that if you plan to run borgmatic on a schedule with cron, and you +encrypt your Borg repository with a passphrase instead of a key file, you'll +either need to set the borgmatic `encryption_passphrase` configuration +variable or set the `BORG_PASSPHRASE` environment variable. See the +[repository encryption +section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption) +of the Quick Start for more info. + +Alternatively, the passphrase can be specified programatically by setting +either the borgmatic `encryption_passcommand` configuration variable or the +`BORG_PASSCOMMAND` environment variable. See the [Borg Security +FAQ](http://borgbackup.readthedocs.io/en/stable/faq.html#how-can-i-specify-the-encryption-passphrase-programmatically) +for more info. + + +## Usage + +### Initialization + +Before you can create backups with borgmatic, you first need to initialize a +Borg repository so you have a destination for your backup archives. (But skip +this step if you already have a Borg repository.) To create a repository, run +a command like the following: + +```bash +borgmatic --init --encryption repokey +``` + +This uses the borgmatic configuration file you created above to determine +which local or remote repository to create, and encrypts it with the +encryption passphrase specified there if one is provided. Read about [Borg +encryption +modes](https://borgbackup.readthedocs.io/en/latest/usage/init.html#encryption-modes) +for the menu of available encryption modes. + +Also, optionally check out the [Borg Quick +Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) for more +background about repository initialization. + +If the repository is on a remote host, make sure that your local user has +key-based SSH access to the desired user account on the remote host. + + +### Backups + +You can run borgmatic and start a backup simply by invoking it without +arguments: + +```bash +borgmatic +``` + +This will also prune any old backups as per the configured retention policy, +and check backups for consistency problems due to things like file damage. + +If you'd like to see the available command-line arguments, view the help: + +```bash +borgmatic --help +``` + +Note that borgmatic prunes archives *before* creating an archive, so as to +free up space for archiving. This means that when a borgmatic run finishes, +there may still be prune-able archives. Not to worry, as they will get cleaned +up at the start of the next run. + + +### Verbosity + +By default, the backup will proceed silently except in the case of errors. But +if you'd like to to get additional information about the progress of the +backup as it proceeds, use the verbosity option: + +```bash +borgmatic --verbosity 1 +``` + +Or, for even more progress spew: + +```bash +borgmatic --verbosity 2 +``` + +### À la carte + +If you want to run borgmatic with only pruning, creating, or checking enabled, +the following optional flags are available: + +```bash +borgmatic --prune +borgmatic --create +borgmatic --check +``` + +You can run with only one of these flags provided, or you can mix and match +any number of them. This supports use cases like running consistency checks +from a different cron job with a different frequency, or running pruning with +a different verbosity level. + +Additionally, borgmatic provides convenient flags for Borg's +[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and +[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html) +functionality: + + +```bash +borgmatic --list +borgmatic --info +``` + +You can include an optional `--json` flag with `--create`, `--list`, or +`--info` to get the output formatted as JSON. + + +## Autopilot + +If you want to run borgmatic automatically, say once a day, the you can +configure a job runner to invoke it periodically. + +### cron + +If you're using cron, download the [sample cron +file](https://projects.torsion.org/witten/borgmatic/src/master/sample/cron/borgmatic). +Then, from the directory where you downloaded it: + +```bash +sudo mv borgmatic /etc/cron.d/borgmatic +sudo chmod +x /etc/cron.d/borgmatic +``` + +You can modify the cron file if you'd like to run borgmatic more or less frequently. + +### systemd + +If you're using systemd instead of cron to run jobs, download the [sample +systemd service +file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.service) +and the [sample systemd timer +file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.timer). +Then, from the directory where you downloaded them: + +```bash +sudo mv borgmatic.service borgmatic.timer /etc/systemd/system/ +sudo systemctl enable borgmatic.timer +sudo systemctl start borgmatic.timer +``` + +Feel free to modify the timer file based on how frequently you'd like +borgmatic to run. + + +## Advanced configuration + ### Multiple configuration files A more advanced usage is to create multiple separate configuration files and @@ -247,113 +385,6 @@ That's it! borgmatic will continue using your /etc/borgmatic configuration files. -## Usage - -You can run borgmatic and start a backup simply by invoking it without -arguments: - -```bash -borgmatic -``` - -This will also prune any old backups as per the configured retention policy, -and check backups for consistency problems due to things like file damage. - -If you'd like to see the available command-line arguments, view the help: - -```bash -borgmatic --help -``` - -Note that borgmatic prunes archives *before* creating an archive, so as to -free up space for archiving. This means that when a borgmatic run finishes, -there may still be prune-able archives. Not to worry, as they will get cleaned -up at the start of the next run. - -### Verbosity - -By default, the backup will proceed silently except in the case of errors. But -if you'd like to to get additional information about the progress of the -backup as it proceeds, use the verbosity option: - -```bash -borgmatic --verbosity 1 -``` - -Or, for even more progress spew: - -```bash -borgmatic --verbosity 2 -``` - -### À la carte - -If you want to run borgmatic with only pruning, creating, or checking enabled, -the following optional flags are available: - -```bash -borgmatic --prune -borgmatic --create -borgmatic --check -``` - -You can run with only one of these flags provided, or you can mix and match -any number of them. This supports use cases like running consistency checks -from a different cron job with a different frequency, or running pruning with -a different verbosity level. - -Additionally, borgmatic provides convenient flags for Borg's -[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and -[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html) -functionality: - - -```bash -borgmatic --list -borgmatic --info -``` - -You can include an optional `--json` flag with `--create`, `--list`, or -`--info` to get the output formatted as JSON. - - -## Autopilot - -If you want to run borgmatic automatically, say once a day, the you can -configure a job runner to invoke it periodically. - -### cron - -If you're using cron, download the [sample cron -file](https://projects.torsion.org/witten/borgmatic/src/master/sample/cron/borgmatic). -Then, from the directory where you downloaded it: - -```bash -sudo mv borgmatic /etc/cron.d/borgmatic -sudo chmod +x /etc/cron.d/borgmatic -``` - -You can modify the cron file if you'd like to run borgmatic more or less frequently. - -### systemd - -If you're using systemd instead of cron to run jobs, download the [sample -systemd service -file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.service) -and the [sample systemd timer -file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.timer). -Then, from the directory where you downloaded them: - -```bash -sudo mv borgmatic.service borgmatic.timer /etc/systemd/system/ -sudo systemctl enable borgmatic.timer -sudo systemctl start borgmatic.timer -``` - -Feel free to modify the timer file based on how frequently you'd like -borgmatic to run. - - ## Support and contributing ### Issues diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 2b0af39..a446380 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -9,20 +9,6 @@ import tempfile logger = logging.getLogger(__name__) -def initialize_environment(storage_config): - passcommand = storage_config.get('encryption_passcommand') - if passcommand: - os.environ['BORG_PASSCOMMAND'] = passcommand - - passphrase = storage_config.get('encryption_passphrase') - if passphrase: - os.environ['BORG_PASSPHRASE'] = passphrase - - ssh_command = storage_config.get('ssh_command') - if ssh_command: - os.environ['BORG_RSH'] = ssh_command - - def _expand_directory(directory): ''' Given a directory path, expand any tilde (representing a user's home directory) and any globs diff --git a/borgmatic/borg/environment.py b/borgmatic/borg/environment.py new file mode 100644 index 0000000..0f717e7 --- /dev/null +++ b/borgmatic/borg/environment.py @@ -0,0 +1,15 @@ +import os + + +def initialize(storage_config): + passcommand = storage_config.get('encryption_passcommand') + if passcommand: + os.environ['BORG_PASSCOMMAND'] = passcommand + + passphrase = storage_config.get('encryption_passphrase') + if passphrase: + os.environ['BORG_PASSPHRASE'] = passphrase + + ssh_command = storage_config.get('ssh_command') + if ssh_command: + os.environ['BORG_RSH'] = ssh_command diff --git a/borgmatic/borg/init.py b/borgmatic/borg/init.py new file mode 100644 index 0000000..b9bdf9a --- /dev/null +++ b/borgmatic/borg/init.py @@ -0,0 +1,31 @@ +import logging +import subprocess + + +logger = logging.getLogger(__name__) + + +def initialize_repository( + repository, + encryption_mode, + append_only=None, + storage_quota=None, + local_path='borg', + remote_path=None, +): + ''' + Given a local or remote repository path, a Borg encryption mode, whether the repository should + be append-only, and the storage quota to use, initialize the repository. + ''' + full_command = ( + (local_path, 'init', repository) + + (('--encryption', encryption_mode) if encryption_mode else ()) + + (('--append-only',) if append_only else ()) + + (('--storage-quota', storage_quota) if storage_quota else ()) + + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + + (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ()) + + (('--remote-path', remote_path) if remote_path else ()) + ) + + logger.debug(' '.join(full_command)) + subprocess.check_call(full_command) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 78eef23..6be3933 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -8,9 +8,11 @@ import sys from borgmatic.borg import ( check as borg_check, create as borg_create, + environment as borg_environment, prune as borg_prune, list as borg_list, info as borg_info, + init as borg_init, ) from borgmatic.commands import hook from borgmatic.config import checks, collect, convert, validate @@ -53,6 +55,26 @@ def parse_arguments(*arguments): dest='excludes_filename', help='Deprecated in favor of exclude_patterns within configuration', ) + parser.add_argument( + '-I', '--init', dest='init', action='store_true', help='Initialize an empty Borg repository' + ) + parser.add_argument( + '-e', + '--encryption', + dest='encryption_mode', + help='Borg repository encryption mode (for use with --init)', + ) + parser.add_argument( + '--append-only', + dest='append_only', + action='store_true', + help='Create an append-only repository (for use with --init)', + ) + parser.add_argument( + '--storage-quota', + dest='storage_quota', + help='Create a repository with a fixed storage quota (for use with --init)', + ) parser.add_argument( '-p', '--prune', @@ -111,7 +133,21 @@ def parse_arguments(*arguments): args = parser.parse_args(arguments) if args.excludes_filename: - raise ValueError('The --excludes option has been replaced with exclude_patterns in configuration') + raise ValueError( + 'The --excludes option has been replaced with exclude_patterns in configuration' + ) + + if (args.encryption_mode or args.append_only or args.storage_quota) and not args.init: + raise ValueError( + 'The --encryption, --append-only, and --storage-quota options can only be used with the --init option' + ) + + if args.init and (args.prune or args.create or args.dry_run): + raise ValueError( + 'The --init option cannot be used with the --prune, --create, or --dry-run options' + ) + if args.init and not args.encryption_mode: + raise ValueError('The --encryption option is required with the --init option') if args.progress and not args.create: raise ValueError('The --progress option can only be used with the --create option') @@ -128,7 +164,7 @@ def parse_arguments(*arguments): # If any of the action flags are explicitly requested, leave them as-is. Otherwise, assume # defaults: Mutate the given arguments to enable the default actions. - if args.prune or args.create or args.check or args.list or args.info: + if args.init or args.prune or args.create or args.check or args.list or args.info: return args args.prune = True @@ -152,7 +188,7 @@ def run_configuration(config_filename, args): # pragma: no cover try: local_path = location.get('local_path', 'borg') remote_path = location.get('remote_path') - borg_create.initialize_environment(storage) + borg_environment.initialize(storage) if args.create: hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup') @@ -206,6 +242,16 @@ def _run_commands_on_repository( ): # pragma: no cover repository = os.path.expanduser(unexpanded_repository) dry_run_label = ' (dry run; not making any changes)' if args.dry_run else '' + if args.init: + logger.info('{}: Initializing repository'.format(repository)) + borg_init.initialize_repository( + repository, + args.encryption_mode, + args.append_only, + args.storage_quota, + local_path=local_path, + remote_path=remote_path, + ) if args.prune: logger.info('{}: Pruning archives{}'.format(repository, dry_run_label)) borg_prune.prune_archives( diff --git a/setup.py b/setup.py index 5f9650d..996067a 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.2.12.dev0' +VERSION = '1.2.12' setup( diff --git a/tests/end-to-end/test_borgmatic.py b/tests/end-to-end/test_borgmatic.py index a0314aa..76802d3 100644 --- a/tests/end-to-end/test_borgmatic.py +++ b/tests/end-to-end/test_borgmatic.py @@ -9,7 +9,8 @@ import tempfile def generate_configuration(config_path, repository_path): ''' Generate borgmatic configuration into a file at the config path, and update the defaults so as - to work for testing (including injecting the given repository path). + to work for testing (including injecting the given repository path and tacking on an encryption + passphrase). ''' subprocess.check_call( 'generate-borgmatic-config --destination {}'.format(config_path).split(' ') @@ -21,6 +22,7 @@ def generate_configuration(config_path, repository_path): .replace('- /home', '- {}'.format(config_path)) .replace('- /etc', '') .replace('- /var/log/syslog*', '') + + 'storage:\n encryption_passphrase: "test"' ) config_file = open(config_path, 'w') config_file.write(config) @@ -33,14 +35,13 @@ def test_borgmatic_command(): repository_path = os.path.join(temporary_directory, 'test.borg') try: - subprocess.check_call( - 'borg init --encryption repokey {}'.format(repository_path).split(' '), - env={'BORG_PASSPHRASE': '', **os.environ}, - ) - config_path = os.path.join(temporary_directory, 'test.yaml') generate_configuration(config_path, repository_path) + subprocess.check_call( + 'borgmatic -v 2 --config {} --init --encryption repokey'.format(config_path).split(' ') + ) + # Run borgmatic to generate a backup archive, and then list it to make sure it exists. subprocess.check_call('borgmatic --config {}'.format(config_path).split(' ')) output = subprocess.check_output( diff --git a/tests/integration/commands/test_borgmatic.py b/tests/integration/commands/test_borgmatic.py index 66ffc5e..cbbd4a0 100644 --- a/tests/integration/commands/test_borgmatic.py +++ b/tests/integration/commands/test_borgmatic.py @@ -16,13 +16,6 @@ def test_parse_arguments_with_no_arguments_uses_defaults(): assert parser.json is False -def test_parse_arguments_disallows_deprecated_excludes_option(): - flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) - - with pytest.raises(ValueError): - module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes') - - def test_parse_arguments_with_multiple_config_paths_parses_as_list(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) @@ -32,7 +25,7 @@ def test_parse_arguments_with_multiple_config_paths_parses_as_list(): assert parser.verbosity is 0 -def test_parse_arguments_with_verbosity_flag_overrides_default(): +def test_parse_arguments_with_verbosity_overrides_default(): config_paths = ['default'] flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths) @@ -43,7 +36,7 @@ def test_parse_arguments_with_verbosity_flag_overrides_default(): assert parser.verbosity == 1 -def test_parse_arguments_with_json_flag_overrides_default(): +def test_parse_arguments_with_json_overrides_default(): parser = module.parse_arguments('--list', '--json') assert parser.json is True @@ -85,25 +78,81 @@ def test_parse_arguments_with_invalid_arguments_exits(): module.parse_arguments('--posix-me-harder') -def test_parse_arguments_with_progress_and_create_flags_does_not_raise(): +def test_parse_arguments_disallows_deprecated_excludes_option(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes') + + +def test_parse_arguments_disallows_encryption_mode_without_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--encryption', 'repokey') + + +def test_parse_arguments_requires_encryption_mode_with_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--init') + + +def test_parse_arguments_disallows_append_only_without_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--append-only') + + +def test_parse_arguments_disallows_storage_quota_without_init(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G') + + +def test_parse_arguments_disallows_init_and_prune(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--init', '--prune') + + +def test_parse_arguments_disallows_init_and_create(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--init', '--create') + + +def test_parse_arguments_disallows_init_and_dry_run(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments('--config', 'myconfig', '--init', '--dry-run') + + +def test_parse_arguments_allows_progress_and_create(): module.parse_arguments('--progress', '--create', '--list') -def test_parse_arguments_with_progress_flag_but_no_create_flag_raises_value_error(): +def test_parse_arguments_disallows_progress_without_create(): with pytest.raises(ValueError): module.parse_arguments('--progress', '--list') -def test_parse_arguments_with_json_flag_with_list_or_info_flag_does_not_raise_any_error(): +def test_parse_arguments_allows_json_with_list_or_info(): module.parse_arguments('--list', '--json') module.parse_arguments('--info', '--json') -def test_parse_arguments_with_json_flag_but_no_list_or_info_flag_raises_value_error(): +def test_parse_arguments_disallows_json_without_list_or_info(): with pytest.raises(ValueError): module.parse_arguments('--json') -def test_parse_arguments_with_json_flag_and_both_list_and_info_flag_raises_value_error(): +def test_parse_arguments_disallows_json_with_both_list_and_info(): with pytest.raises(ValueError): module.parse_arguments('--list', '--info', '--json') diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index f8840e5..14ccbf7 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -1,5 +1,4 @@ import logging -import os from flexmock import flexmock @@ -7,52 +6,6 @@ from borgmatic.borg import create as module from ..test_verbosity import insert_logging_mock -def test_initialize_environment_with_passcommand_should_set_environment(): - orig_environ = os.environ - - try: - os.environ = {} - module.initialize_environment({'encryption_passcommand': 'command'}) - assert os.environ.get('BORG_PASSCOMMAND') == 'command' - finally: - os.environ = orig_environ - - -def test_initialize_environment_with_passphrase_should_set_environment(): - orig_environ = os.environ - - try: - os.environ = {} - module.initialize_environment({'encryption_passphrase': 'pass'}) - assert os.environ.get('BORG_PASSPHRASE') == 'pass' - finally: - os.environ = orig_environ - - -def test_initialize_environment_with_ssh_command_should_set_environment(): - orig_environ = os.environ - - try: - os.environ = {} - module.initialize_environment({'ssh_command': 'ssh -C'}) - assert os.environ.get('BORG_RSH') == 'ssh -C' - finally: - os.environ = orig_environ - - -def test_initialize_environment_without_configuration_should_not_set_environment(): - orig_environ = os.environ - - try: - os.environ = {} - module.initialize_environment({}) - assert os.environ.get('BORG_PASSCOMMAND') is None - assert os.environ.get('BORG_PASSPHRASE') is None - assert os.environ.get('BORG_RSH') is None - finally: - os.environ = orig_environ - - def test_expand_directory_with_basic_path_passes_it_through(): flexmock(module.os.path).should_receive('expanduser').and_return('foo') flexmock(module.glob).should_receive('glob').and_return([]) diff --git a/tests/unit/borg/test_environment.py b/tests/unit/borg/test_environment.py new file mode 100644 index 0000000..67653b3 --- /dev/null +++ b/tests/unit/borg/test_environment.py @@ -0,0 +1,49 @@ +import os + +from borgmatic.borg import environment as module + + +def test_initialize_with_passcommand_should_set_environment(): + orig_environ = os.environ + + try: + os.environ = {} + module.initialize({'encryption_passcommand': 'command'}) + assert os.environ.get('BORG_PASSCOMMAND') == 'command' + finally: + os.environ = orig_environ + + +def test_initialize_with_passphrase_should_set_environment(): + orig_environ = os.environ + + try: + os.environ = {} + module.initialize({'encryption_passphrase': 'pass'}) + assert os.environ.get('BORG_PASSPHRASE') == 'pass' + finally: + os.environ = orig_environ + + +def test_initialize_with_ssh_command_should_set_environment(): + orig_environ = os.environ + + try: + os.environ = {} + module.initialize({'ssh_command': 'ssh -C'}) + assert os.environ.get('BORG_RSH') == 'ssh -C' + finally: + os.environ = orig_environ + + +def test_initialize_without_configuration_should_not_set_environment(): + orig_environ = os.environ + + try: + os.environ = {} + module.initialize({}) + assert os.environ.get('BORG_PASSCOMMAND') is None + assert os.environ.get('BORG_PASSPHRASE') is None + assert os.environ.get('BORG_RSH') is None + finally: + os.environ = orig_environ diff --git a/tests/unit/borg/test_init.py b/tests/unit/borg/test_init.py new file mode 100644 index 0000000..67ed8a8 --- /dev/null +++ b/tests/unit/borg/test_init.py @@ -0,0 +1,58 @@ +import logging + +from flexmock import flexmock + +from borgmatic.borg import init as module +from ..test_verbosity import insert_logging_mock + + +def insert_subprocess_mock(check_call_command, **kwargs): + subprocess = flexmock(module.subprocess) + subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once() + + +INIT_COMMAND = ('borg', 'init', 'repo', '--encryption', 'repokey') + + +def test_initialize_repository_calls_borg_with_parameters(): + insert_subprocess_mock(INIT_COMMAND) + + module.initialize_repository(repository='repo', encryption_mode='repokey') + + +def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter(): + insert_subprocess_mock(INIT_COMMAND + ('--append-only',)) + + module.initialize_repository(repository='repo', encryption_mode='repokey', append_only=True) + + +def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter(): + insert_subprocess_mock(INIT_COMMAND + ('--storage-quota', '5G')) + + module.initialize_repository(repository='repo', encryption_mode='repokey', storage_quota='5G') + + +def test_initialize_repository_with_log_info_calls_borg_with_info_parameter(): + insert_subprocess_mock(INIT_COMMAND + ('--info',)) + insert_logging_mock(logging.INFO) + + module.initialize_repository(repository='repo', encryption_mode='repokey') + + +def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter(): + insert_subprocess_mock(INIT_COMMAND + ('--debug',)) + insert_logging_mock(logging.DEBUG) + + module.initialize_repository(repository='repo', encryption_mode='repokey') + + +def test_initialize_repository_with_local_path_calls_borg_via_local_path(): + insert_subprocess_mock(('borg1',) + INIT_COMMAND[1:]) + + module.initialize_repository(repository='repo', encryption_mode='repokey', local_path='borg1') + + +def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter(): + insert_subprocess_mock(INIT_COMMAND + ('--remote-path', 'borg1')) + + module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1') diff --git a/tests/unit/test_verbosity.py b/tests/unit/test_verbosity.py index cb1f3af..9069caa 100644 --- a/tests/unit/test_verbosity.py +++ b/tests/unit/test_verbosity.py @@ -6,9 +6,11 @@ from borgmatic import verbosity as module def insert_logging_mock(log_level): - """ Mocks the isEnabledFor from python logging. """ + ''' + Mock the isEnabledFor from Python logging. + ''' logging = flexmock(module.logging.Logger) - logging.should_receive('isEnabledFor').replace_with(lambda lvl: lvl >= log_level) + logging.should_receive('isEnabledFor').replace_with(lambda level: level >= log_level) logging.should_receive('getEffectiveLevel').replace_with(lambda: log_level)