diff --git a/borgmatic/actions/restore.py b/borgmatic/actions/restore.py index cf5f6eef..632c0183 100644 --- a/borgmatic/actions/restore.py +++ b/borgmatic/actions/restore.py @@ -176,7 +176,13 @@ def restore_single_dump( that data source from the archive. ''' dump_metadata = render_dump_metadata( - Dump(hook_name, data_source['name'], data_source.get('hostname'), data_source.get('port'), data_source.get('label')), + Dump( + hook_name, + data_source['name'], + data_source.get('hostname'), + data_source.get('port'), + data_source.get('label'), + ), ) logger.info(f'Restoring data source {dump_metadata}') @@ -568,7 +574,13 @@ def run_restore( if not found_data_source: found_data_source = get_configured_data_source( config, - Dump(restore_dump.hook_name, 'all', restore_dump.hostname, restore_dump.port, restore_dump.label), + Dump( + restore_dump.hook_name, + 'all', + restore_dump.hostname, + restore_dump.port, + restore_dump.label, + ), ) if not found_data_source: diff --git a/borgmatic/hooks/data_source/utils.py b/borgmatic/hooks/data_source/utils.py index a788ce8e..6c50472d 100644 --- a/borgmatic/hooks/data_source/utils.py +++ b/borgmatic/hooks/data_source/utils.py @@ -9,6 +9,7 @@ IS_A_HOOK = False logger = logging.getLogger(__name__) + def resolve_database_option(option, data_source, connection_params=None, restore=False): # Special case `hostname` since it overlaps with `container` if option == 'hostname': @@ -24,7 +25,7 @@ def _get_hostname_from_config(data_source, connection_params=None, restore=False # connection params win, full stop if connection_params: if container := connection_params.get('container'): - return container + return get_ip_from_container(container) if hostname := connection_params.get('hostname'): return hostname # ... then try the restore config diff --git a/tests/unit/actions/test_restore.py b/tests/unit/actions/test_restore.py index 602c30b4..106d1e01 100644 --- a/tests/unit/actions/test_restore.py +++ b/tests/unit/actions/test_restore.py @@ -1102,7 +1102,7 @@ def test_run_restore_restores_each_data_source(): username=None, password=None, restore_path=None, - container=None + container=None, ), global_arguments=flexmock(dry_run=False), local_path=flexmock(), diff --git a/tests/unit/hooks/data_source/test_utils.py b/tests/unit/hooks/data_source/test_utils.py new file mode 100644 index 00000000..ef7c0b2c --- /dev/null +++ b/tests/unit/hooks/data_source/test_utils.py @@ -0,0 +1,91 @@ +from subprocess import CalledProcessError + +import pytest +from flexmock import flexmock + +from borgmatic.hooks.data_source import utils + + +def test_get_database_option(): + data_source = {'option': 'original_value', 'restore_option': 'restore_value'} + connection_params = {'option': 'connection_value'} + + assert utils.resolve_database_option('option', data_source) == 'original_value' + assert utils.resolve_database_option('option', data_source, restore=True) == 'restore_value' + assert ( + utils.resolve_database_option('option', data_source, connection_params) + == 'connection_value' + ) + assert ( + utils.resolve_database_option('option', data_source, connection_params, restore=True) + == 'connection_value' + ) + + +def test_get_hostname_option_via_container(): + data_source = { + 'container': 'original_container', + 'hostname': 'original_hostname', + 'restore_container': 'restore_container', + 'restore_hostname': 'restore_hostname', + } + connection_params = {'container': 'connection_container', 'hostname': 'connection_hostname'} + + flexmock(utils).should_receive('get_ip_from_container').with_args( + 'original_container' + ).and_return('container_ip_1') + flexmock(utils).should_receive('get_ip_from_container').with_args( + 'connection_container' + ).and_return('container_ip_2') + flexmock(utils).should_receive('get_ip_from_container').with_args( + 'restore_container' + ).and_return('container_ip_3') + + assert utils.resolve_database_option('hostname', data_source) == 'container_ip_1' + assert ( + utils.resolve_database_option('hostname', data_source, connection_params) + == 'container_ip_2' + ) + assert utils.resolve_database_option('hostname', data_source, restore=True) == 'container_ip_3' + + +def test_get_container_ip_without_engines(): + flexmock(utils.shutil).should_receive('which').and_return(None).and_return(None) + + with pytest.raises(ValueError): + utils.get_ip_from_container('yolo') + + +def test_get_container_ip_success(): + flexmock(utils.shutil).should_receive('which').and_return(None).and_return('/usr/bin/podman') + + flexmock(utils).should_receive('execute_command_and_capture_output').and_return( + '{"IPAddress": "1.2.3.4"}' + ) + + addr = utils.get_ip_from_container('yolo') + assert addr == '1.2.3.4' + + flexmock(utils).should_receive('execute_command_and_capture_output').and_return( + '{"Networks": {"my_network": {"IPAddress": "5.6.7.8"}}}' + ) + + assert utils.get_ip_from_container('yolo') == '5.6.7.8' + + +def test_get_container_ip_container_not_found(): + flexmock(utils.shutil).should_receive('which').and_return('/usr/bin/podman') + flexmock(utils).should_receive('execute_command_and_capture_output').and_raise( + CalledProcessError, 1, ['/usr/bin/podman', 'inspect', 'yolo'], None, 'No such object' + ) + + with pytest.raises(CalledProcessError): + utils.get_ip_from_container('does not exist') + + +def test_get_container_ip_container_no_network(): + flexmock(utils.shutil).should_receive('which').and_return(None).and_return('/usr/bin/podman') + + flexmock(utils).should_receive('execute_command_and_capture_output').and_return('{}') + + assert utils.get_ip_from_container('yolo') is None