Add "key export" action to export a copy of the repository key (#345).
This commit is contained in:
parent
fd8c56c6be
commit
6dca7c1c15
9 changed files with 426 additions and 3 deletions
6
NEWS
6
NEWS
|
@ -1,9 +1,11 @@
|
|||
1.8.2.dev0
|
||||
* #345: Add "key export" action to export a copy of the repository key for safekeeping in case
|
||||
the original goes missing or gets damaged.
|
||||
* #727: Add a MariaDB database hook that uses native MariaDB commands instead of the deprecated
|
||||
MySQL ones. Be aware though that any existing backups made with the "mysql_databases:" hook are
|
||||
only restorable with a "mysql_databases:" configuration.
|
||||
* Add a source code reference for getting oriented with the borgmatic code as a developer:
|
||||
https://torsion.org/borgmatic/docs/reference/source-code/
|
||||
* Add source code reference documentation for getting oriented with the borgmatic code as a
|
||||
developer: https://torsion.org/borgmatic/docs/reference/source-code/
|
||||
|
||||
1.8.1
|
||||
* #326: Add documentation for restoring a database to an alternate host:
|
||||
|
|
33
borgmatic/actions/export_key.py
Normal file
33
borgmatic/actions/export_key.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import logging
|
||||
|
||||
import borgmatic.borg.export_key
|
||||
import borgmatic.config.validate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_export_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
export_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Run the "key export" action for the given repository.
|
||||
'''
|
||||
if export_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||
repository, export_arguments.repository
|
||||
):
|
||||
logger.info(f'{repository.get("label", repository["path"])}: Exporting repository key')
|
||||
borgmatic.borg.export_key.export_key(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
export_arguments,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
70
borgmatic/borg/export_key.py
Normal file
70
borgmatic/borg/export_key.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, flags
|
||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def export_key(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
export_arguments,
|
||||
global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, and
|
||||
optional local and remote Borg paths, export the repository key to the destination path
|
||||
indicated in the export arguments.
|
||||
|
||||
If the destination path is empty or "-", then print the key to stdout instead of to a file.
|
||||
|
||||
Raise FileExistsError if a path is given but it already exists on disk.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
|
||||
if export_arguments.path and export_arguments.path != '-':
|
||||
if os.path.exists(export_arguments.path):
|
||||
raise FileExistsError(
|
||||
f'Destination path {export_arguments.path} already exists. Aborting.'
|
||||
)
|
||||
|
||||
output_file = None
|
||||
else:
|
||||
output_file = DO_NOT_CAPTURE
|
||||
|
||||
full_command = (
|
||||
(local_path, 'key', 'export')
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--log-json',) if global_arguments.log_json else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ flags.make_flags('paper', export_arguments.paper)
|
||||
+ flags.make_flags('qr-html', export_arguments.qr_html)
|
||||
+ flags.make_repository_flags(
|
||||
repository_path,
|
||||
local_borg_version,
|
||||
)
|
||||
+ ((export_arguments.path,) if output_file is None else ())
|
||||
)
|
||||
|
||||
if global_arguments.dry_run:
|
||||
logging.info(f'{repository_path}: Skipping key export (dry run)')
|
||||
return
|
||||
|
||||
execute_command(
|
||||
full_command,
|
||||
output_file=output_file,
|
||||
output_log_level=logging.ANSWER,
|
||||
borg_local_path=local_path,
|
||||
extra_environment=environment.make_environment(config),
|
||||
)
|
|
@ -23,6 +23,7 @@ ACTION_ALIASES = {
|
|||
'info': ['-i'],
|
||||
'transfer': [],
|
||||
'break-lock': [],
|
||||
'key': [],
|
||||
'borg': [],
|
||||
}
|
||||
|
||||
|
@ -1176,6 +1177,51 @@ def make_parsers():
|
|||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
key_parser = action_parsers.add_parser(
|
||||
'key',
|
||||
aliases=ACTION_ALIASES['key'],
|
||||
help='Perform repository key related operations',
|
||||
description='Perform repository key related operations',
|
||||
add_help=False,
|
||||
)
|
||||
|
||||
key_group = key_parser.add_argument_group('key arguments')
|
||||
key_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
key_parsers = key_parser.add_subparsers(
|
||||
title='key sub-actions',
|
||||
)
|
||||
|
||||
key_export_parser = key_parsers.add_parser(
|
||||
'export',
|
||||
help='Export a copy of the repository key for safekeeping in case the original goes missing or gets damaged',
|
||||
description='Export a copy of the repository key for safekeeping in case the original goes missing or gets damaged',
|
||||
add_help=False,
|
||||
)
|
||||
key_export_group = key_export_parser.add_argument_group('key export arguments')
|
||||
key_export_group.add_argument(
|
||||
'--paper',
|
||||
action='store_true',
|
||||
help='Export the key in a text format suitable for printing and later manual entry',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'--qr-html',
|
||||
action='store_true',
|
||||
help='Export the key in an HTML format suitable for printing and later manual entry or QR code scanning',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository to export the key for, defaults to the configured repository if there is only one',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'--path',
|
||||
metavar='PATH',
|
||||
help='Path to export the key to, defaults to stdout (but be careful about dirtying the output with --verbosity)',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
borg_parser = action_parsers.add_parser(
|
||||
'borg',
|
||||
aliases=ACTION_ALIASES['borg'],
|
||||
|
|
|
@ -22,6 +22,7 @@ import borgmatic.actions.config.bootstrap
|
|||
import borgmatic.actions.config.generate
|
||||
import borgmatic.actions.config.validate
|
||||
import borgmatic.actions.create
|
||||
import borgmatic.actions.export_key
|
||||
import borgmatic.actions.export_tar
|
||||
import borgmatic.actions.extract
|
||||
import borgmatic.actions.info
|
||||
|
@ -448,6 +449,16 @@ def run_actions(
|
|||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'export':
|
||||
borgmatic.actions.export_key.run_export_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'borg':
|
||||
borgmatic.actions.borg.run_borg(
|
||||
repository,
|
||||
|
|
|
@ -310,7 +310,8 @@ problem: the `restore` action figures out which repository to use.
|
|||
|
||||
But if you have multiple repositories configured, then you'll need to specify
|
||||
the repository to use via the `--repository` flag. This can be done either
|
||||
with the repository's path or its label as configured in your borgmatic configuration file.
|
||||
with the repository's path or its label as configured in your borgmatic
|
||||
configuration file.
|
||||
|
||||
```bash
|
||||
borgmatic restore --repository repo.borg --archive host-2023-...
|
||||
|
|
20
tests/unit/actions/test_export_key.py
Normal file
20
tests/unit/actions/test_export_key.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions import export_key as module
|
||||
|
||||
|
||||
def test_run_export_key_does_not_raise():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
flexmock(module.borgmatic.borg.export_key).should_receive('export_key')
|
||||
export_arguments = flexmock(repository=flexmock())
|
||||
|
||||
module.run_export_key(
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
export_arguments=export_arguments,
|
||||
global_arguments=flexmock(),
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
222
tests/unit/borg/test_export_key.py
Normal file
222
tests/unit/borg/test_export_key.py
Normal file
|
@ -0,0 +1,222 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import export_key as module
|
||||
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(command, output_file=module.DO_NOT_CAPTURE):
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command,
|
||||
output_file=output_file,
|
||||
output_log_level=module.logging.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_required_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_remote_path_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--remote-path', 'borg1', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_umask_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--umask', '0770', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={'umask': '0770'},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_log_json_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--log-json', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=True),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_lock_wait_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--lock-wait', '5', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={'lock_wait': '5'},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--info', 'repo'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_log_debug_calls_borg_with_debug_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--debug', '--show-rc', 'repo'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_paper_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--paper', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=True, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_paper_flag():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--paper', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=True, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_qr_html_flag():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--qr-html', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=True, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_path_argument():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
insert_execute_command_mock(('borg', 'key', 'export', 'repo', 'dest'), output_file=None)
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path='dest'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_already_existent_path_raises():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
with pytest.raises(FileExistsError):
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path='dest'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_stdout_path_calls_borg_without_path_argument():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path='-'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_dry_run_skip_borg_call():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=True, log_json=False),
|
||||
)
|
|
@ -748,6 +748,24 @@ def test_run_actions_runs_break_lock():
|
|||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_export_key():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(borgmatic.actions.export_key).should_receive('run_export_key').once()
|
||||
|
||||
tuple(
|
||||
module.run_actions(
|
||||
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'export': flexmock()},
|
||||
config_filename=flexmock(),
|
||||
config={'repositories': []},
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_borg():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
|
|
Loading…
Reference in a new issue