Files
patroni/tests/test_barman.py
Israel 014777b20a Refactor Barman scripts and add a sub-command to switch Barman config (#3016)
We currently have a script named `patroni_barman_recover` in Patroni, which is intended to be used as a custom bootstrap method, or as a custom replica creation method.

Now there is need of one more Barman related script in Patroni to handle switching of config models in Barman upon `on_role_change` events.

However, instead of creating another Patroni script, let's say `patroni_barman_config_switch`, and duplicating a lot of logic in the code, we decided to refactor the code so:

* Instead of two separate scripts (`patroni_barman_recover` and `patroni_barman_config_switch`), we have a single script (`patroni_barman`) with 2 sub-commands (`recover` and `config-switch`)

This is the overview of changes that have been performed:

* File `patroni.scripts.barman_recover` has been removed, and its logic has been split into a few files:
  * `patroni.scripts.barman.cli`: handles the entrypoint of the new `patroni_barman` command, exposing the argument parser and calling the appropriate functions depending on the sub-command
  * `patroni.scripts.barman.utils`: implements utilitary enums, functions and classes wich can be used by `cli` and by sub-commands implementation:
    * retry mechanism
    * logging set up
    * communication with pg-backup-api
  * `patroni.scripts.barman.recover`: implements the `recover` sub-command only
* File `patroni.tests.test_barman_recover` has been renamed as `patroni.tests.test_barman`
* File `patroni.scripts.barman.config_switch` was created to implement the `config-switch` sub-command only
* `setup.py` has been changed so it generates a `patroni_barman` application instead of `patroni_barman_recover`
* Docs and unit tests were updated accordingly

References: PAT-154.
2024-03-20 09:04:55 +01:00

766 lines
30 KiB
Python

import logging
import mock
from mock import MagicMock, Mock, patch
import unittest
from urllib3.exceptions import MaxRetryError
from patroni.scripts.barman.cli import main
from patroni.scripts.barman.config_switch import (ExitCode as BarmanConfigSwitchExitCode, _should_skip_switch,
_switch_config, run_barman_config_switch)
from patroni.scripts.barman.recover import ExitCode as BarmanRecoverExitCode, _restore_backup, run_barman_recover
from patroni.scripts.barman.utils import ApiNotOk, OperationStatus, PgBackupApi, RetriesExceeded, set_up_logging
API_URL = "http://localhost:7480"
BARMAN_SERVER = "my_server"
BARMAN_MODEL = "my_model"
BACKUP_ID = "backup_id"
SSH_COMMAND = "ssh postgres@localhost"
DATA_DIRECTORY = "/path/to/pgdata"
LOOP_WAIT = 10
RETRY_WAIT = 2
MAX_RETRIES = 5
# stuff from patroni.scripts.barman.utils
@patch("logging.basicConfig")
def test_set_up_logging(mock_log_config):
log_file = "/path/to/some/file.log"
set_up_logging(log_file)
mock_log_config.assert_called_once_with(filename=log_file, level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s")
class TestPgBackupApi(unittest.TestCase):
@patch.object(PgBackupApi, "_ensure_api_ok", Mock())
@patch("patroni.scripts.barman.utils.PoolManager", MagicMock())
def setUp(self):
self.api = PgBackupApi(API_URL, None, None, RETRY_WAIT, MAX_RETRIES)
# Reset the mock as the same instance is used across tests
self.api._http.request.reset_mock()
self.api._http.request.side_effect = None
def test__build_full_url(self):
self.assertEqual(self.api._build_full_url("/some/path"), f"{API_URL}/some/path")
@patch("json.loads")
def test__deserialize_response(self, mock_json_loads):
mock_response = MagicMock()
self.assertIsNotNone(self.api._deserialize_response(mock_response))
mock_json_loads.assert_called_once_with(mock_response.data.decode("utf-8"))
@patch("json.dumps")
def test__serialize_request(self, mock_json_dumps):
body = "some_body"
ret = self.api._serialize_request(body)
self.assertIsNotNone(ret)
mock_json_dumps.assert_called_once_with(body)
mock_json_dumps.return_value.encode.assert_called_once_with("utf-8")
@patch.object(PgBackupApi, "_deserialize_response", Mock(return_value="test"))
def test__get_request(self):
mock_request = self.api._http.request
# with no error
self.assertEqual(self.api._get_request("/some/path"), "test")
mock_request.assert_called_once_with("GET", f"{API_URL}/some/path")
# with MaxRetryError
http_error = MaxRetryError(self.api._http, f"{API_URL}/some/path")
mock_request.side_effect = http_error
with self.assertRaises(RetriesExceeded) as exc:
self.assertIsNone(self.api._get_request("/some/path"))
self.assertEqual(
str(exc.exception),
"Failed to perform a GET request to http://localhost:7480/some/path"
)
@patch.object(PgBackupApi, "_deserialize_response", Mock(return_value="test"))
@patch.object(PgBackupApi, "_serialize_request")
def test__post_request(self, mock_serialize):
mock_request = self.api._http.request
# with no error
self.assertEqual(self.api._post_request("/some/path", "some body"), "test")
mock_serialize.assert_called_once_with("some body")
mock_request.assert_called_once_with("POST", f"{API_URL}/some/path", body=mock_serialize.return_value,
headers={"Content-Type": "application/json"})
# with HTTPError
http_error = MaxRetryError(self.api._http, f"{API_URL}/some/path")
mock_request.side_effect = http_error
with self.assertRaises(RetriesExceeded) as exc:
self.assertIsNone(self.api._post_request("/some/path", "some body"))
self.assertEqual(
str(exc.exception),
f"Failed to perform a POST request to http://localhost:7480/some/path with {mock_serialize.return_value}"
)
@patch.object(PgBackupApi, "_get_request")
def test__ensure_api_ok(self, mock_get_request):
# API ok
mock_get_request.return_value = "OK"
self.assertIsNone(self.api._ensure_api_ok())
# API not ok
mock_get_request.return_value = "random"
with self.assertRaises(ApiNotOk) as exc:
self.assertIsNone(self.api._ensure_api_ok())
self.assertEqual(
str(exc.exception),
"pg-backup-api is currently not up and running at http://localhost:7480: random",
)
@patch("patroni.scripts.barman.utils.OperationStatus")
@patch("logging.warning")
@patch("time.sleep")
@patch.object(PgBackupApi, "_get_request")
def test_get_operation_status(self, mock_get_request, mock_sleep, mock_logging, mock_op_status):
# well formed response
mock_get_request.return_value = {"status": "some status"}
mock_op_status.__getitem__.return_value = "SOME_STATUS"
self.assertEqual(self.api.get_operation_status(BARMAN_SERVER, "some_id"), "SOME_STATUS")
mock_get_request.assert_called_once_with(f"servers/{BARMAN_SERVER}/operations/some_id")
mock_sleep.assert_not_called()
mock_logging.assert_not_called()
mock_op_status.__getitem__.assert_called_once_with("some status")
# malformed response
mock_get_request.return_value = {"statuss": "some status"}
with self.assertRaises(RetriesExceeded) as exc:
self.api.get_operation_status(BARMAN_SERVER, "some_id")
self.assertEqual(str(exc.exception),
"Maximum number of retries exceeded for method PgBackupApi.get_operation_status.")
self.assertEqual(mock_sleep.call_count, self.api.max_retries)
mock_sleep.assert_has_calls([mock.call(self.api.retry_wait)] * self.api.max_retries)
self.assertEqual(mock_logging.call_count, self.api.max_retries)
for i in range(mock_logging.call_count):
call_args = mock_logging.call_args_list[i][0]
self.assertEqual(len(call_args), 5)
self.assertEqual(call_args[0], "Attempt %d of %d on method %s failed with %r.")
self.assertEqual(call_args[1], i + 1)
self.assertEqual(call_args[2], self.api.max_retries)
self.assertEqual(call_args[3], "PgBackupApi.get_operation_status")
self.assertIsInstance(call_args[4], KeyError)
self.assertEqual(call_args[4].args, ('status',))
@patch("logging.warning")
@patch("time.sleep")
@patch.object(PgBackupApi, "_post_request")
def test_create_recovery_operation(self, mock_post_request, mock_sleep, mock_logging):
# well formed response
mock_post_request.return_value = {"operation_id": "some_id"}
self.assertEqual(
self.api.create_recovery_operation(BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY),
"some_id",
)
mock_sleep.assert_not_called()
mock_logging.assert_not_called()
mock_post_request.assert_called_once_with(
f"servers/{BARMAN_SERVER}/operations",
{
"type": "recovery",
"backup_id": BACKUP_ID,
"remote_ssh_command": SSH_COMMAND,
"destination_directory": DATA_DIRECTORY,
}
)
# malformed response
mock_post_request.return_value = {"operation_idd": "some_id"}
with self.assertRaises(RetriesExceeded) as exc:
self.api.create_recovery_operation(BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY)
self.assertEqual(str(exc.exception),
"Maximum number of retries exceeded for method PgBackupApi.create_recovery_operation.")
self.assertEqual(mock_sleep.call_count, self.api.max_retries)
mock_sleep.assert_has_calls([mock.call(self.api.retry_wait)] * self.api.max_retries)
self.assertEqual(mock_logging.call_count, self.api.max_retries)
for i in range(mock_logging.call_count):
call_args = mock_logging.call_args_list[i][0]
self.assertEqual(len(call_args), 5)
self.assertEqual(call_args[0], "Attempt %d of %d on method %s failed with %r.")
self.assertEqual(call_args[1], i + 1)
self.assertEqual(call_args[2], self.api.max_retries)
self.assertEqual(call_args[3], "PgBackupApi.create_recovery_operation")
self.assertIsInstance(call_args[4], KeyError)
self.assertEqual(call_args[4].args, ('operation_id',))
@patch("logging.warning")
@patch("time.sleep")
@patch.object(PgBackupApi, "_post_request")
def test_create_config_switch_operation(self, mock_post_request, mock_sleep, mock_logging):
# well formed response -- sample 1
mock_post_request.return_value = {"operation_id": "some_id"}
self.assertEqual(
self.api.create_config_switch_operation(BARMAN_SERVER, BARMAN_MODEL, None),
"some_id",
)
mock_sleep.assert_not_called()
mock_logging.assert_not_called()
mock_post_request.assert_called_once_with(
f"servers/{BARMAN_SERVER}/operations",
{
"type": "config_switch",
"model_name": BARMAN_MODEL,
}
)
# well formed response -- sample 2
mock_post_request.reset_mock()
self.assertEqual(
self.api.create_config_switch_operation(BARMAN_SERVER, None, True),
"some_id",
)
mock_sleep.assert_not_called()
mock_logging.assert_not_called()
mock_post_request.assert_called_once_with(
f"servers/{BARMAN_SERVER}/operations",
{
"type": "config_switch",
"reset": True,
}
)
# malformed response
mock_post_request.return_value = {"operation_idd": "some_id"}
with self.assertRaises(RetriesExceeded) as exc:
self.api.create_config_switch_operation(BARMAN_SERVER, BARMAN_MODEL, None)
self.assertEqual(str(exc.exception),
"Maximum number of retries exceeded for method PgBackupApi.create_config_switch_operation.")
self.assertEqual(mock_sleep.call_count, self.api.max_retries)
mock_sleep.assert_has_calls([mock.call(self.api.retry_wait)] * self.api.max_retries)
self.assertEqual(mock_logging.call_count, self.api.max_retries)
for i in range(mock_logging.call_count):
call_args = mock_logging.call_args_list[i][0]
self.assertEqual(len(call_args), 5)
self.assertEqual(call_args[0], "Attempt %d of %d on method %s failed with %r.")
self.assertEqual(call_args[1], i + 1)
self.assertEqual(call_args[2], self.api.max_retries)
self.assertEqual(call_args[3], "PgBackupApi.create_config_switch_operation")
self.assertIsInstance(call_args[4], KeyError)
self.assertEqual(call_args[4].args, ('operation_id',))
# stuff from patroni.scripts.barman.recover
class TestBarmanRecover(unittest.TestCase):
def setUp(self):
self.api = MagicMock()
# Reset the mock as the same instance is used across tests
self.api._http.request.reset_mock()
self.api._http.request.side_effect = None
@patch("time.sleep")
@patch("logging.info")
@patch("logging.error")
def test__restore_backup(self, mock_log_error, mock_log_info, mock_sleep):
mock_create_op = self.api.create_recovery_operation
mock_get_status = self.api.get_operation_status
# successful fast restore
mock_create_op.return_value = "some_id"
mock_get_status.return_value = OperationStatus.DONE
self.assertEqual(
_restore_backup(self.api, BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY, LOOP_WAIT),
BarmanRecoverExitCode.RECOVERY_DONE,
)
mock_create_op.assert_called_once_with(BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY)
mock_get_status.assert_called_once_with(BARMAN_SERVER, "some_id")
mock_log_info.assert_has_calls([
mock.call("Created the recovery operation with ID %s", "some_id"),
mock.call("Recovery operation finished successfully."),
])
mock_log_error.assert_not_called()
mock_sleep.assert_not_called()
# successful slow restore
mock_create_op.reset_mock()
mock_get_status.reset_mock()
mock_log_info.reset_mock()
mock_get_status.side_effect = [OperationStatus.IN_PROGRESS] * 20 + [OperationStatus.DONE]
self.assertEqual(
_restore_backup(self.api, BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY, LOOP_WAIT),
BarmanRecoverExitCode.RECOVERY_DONE,
)
mock_create_op.assert_called_once()
self.assertEqual(mock_get_status.call_count, 21)
mock_get_status.assert_has_calls([mock.call(BARMAN_SERVER, "some_id")] * 21)
self.assertEqual(mock_log_info.call_count, 22)
mock_log_info.assert_has_calls([mock.call("Created the recovery operation with ID %s", "some_id")]
+ [mock.call("Recovery operation %s is still in progress", "some_id")] * 20
+ [mock.call("Recovery operation finished successfully.")])
mock_log_error.assert_not_called()
self.assertEqual(mock_sleep.call_count, 20)
mock_sleep.assert_has_calls([mock.call(LOOP_WAIT)] * 20)
# failed fast restore
mock_create_op.reset_mock()
mock_get_status.reset_mock()
mock_log_info.reset_mock()
mock_sleep.reset_mock()
mock_get_status.side_effect = None
mock_get_status.return_value = OperationStatus.FAILED
self.assertEqual(
_restore_backup(self.api, BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY, LOOP_WAIT),
BarmanRecoverExitCode.RECOVERY_FAILED,
)
mock_create_op.assert_called_once()
mock_get_status.assert_called_once_with(BARMAN_SERVER, "some_id")
mock_log_info.assert_has_calls([
mock.call("Created the recovery operation with ID %s", "some_id"),
])
mock_log_error.assert_has_calls([
mock.call("Recovery operation failed."),
])
mock_sleep.assert_not_called()
# failed slow restore
mock_create_op.reset_mock()
mock_get_status.reset_mock()
mock_log_info.reset_mock()
mock_log_error.reset_mock()
mock_sleep.reset_mock()
mock_get_status.side_effect = [OperationStatus.IN_PROGRESS] * 20 + [OperationStatus.FAILED]
self.assertEqual(
_restore_backup(self.api, BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY, LOOP_WAIT),
BarmanRecoverExitCode.RECOVERY_FAILED,
)
mock_create_op.assert_called_once()
self.assertEqual(mock_get_status.call_count, 21)
mock_get_status.assert_has_calls([mock.call(BARMAN_SERVER, "some_id")] * 21)
self.assertEqual(mock_log_info.call_count, 21)
mock_log_info.assert_has_calls([mock.call("Created the recovery operation with ID %s", "some_id")]
+ [mock.call("Recovery operation %s is still in progress", "some_id")] * 20)
mock_log_error.assert_has_calls([
mock.call("Recovery operation failed."),
])
self.assertEqual(mock_sleep.call_count, 20)
mock_sleep.assert_has_calls([mock.call(LOOP_WAIT)] * 20)
# create retries exceeded
mock_log_info.reset_mock()
mock_log_error.reset_mock()
mock_sleep.reset_mock()
mock_create_op.side_effect = RetriesExceeded()
mock_get_status.side_effect = None
self.assertEqual(
_restore_backup(self.api, BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY, LOOP_WAIT),
BarmanRecoverExitCode.HTTP_ERROR,
)
mock_log_info.assert_not_called()
mock_log_error.assert_called_once_with("An issue was faced while trying to create a recovery operation: %r",
mock_create_op.side_effect)
mock_sleep.assert_not_called()
# get status retries exceeded
mock_create_op.reset_mock()
mock_create_op.side_effect = None
mock_log_error.reset_mock()
mock_log_info.reset_mock()
mock_get_status.side_effect = RetriesExceeded
self.assertEqual(
_restore_backup(self.api, BARMAN_SERVER, BACKUP_ID, SSH_COMMAND, DATA_DIRECTORY, LOOP_WAIT),
BarmanRecoverExitCode.HTTP_ERROR,
)
mock_log_info.assert_called_once_with("Created the recovery operation with ID %s", "some_id")
mock_log_error.assert_called_once_with("Maximum number of retries exceeded, exiting.")
mock_sleep.assert_not_called()
class TestBarmanRecoverCli(unittest.TestCase):
@patch("patroni.scripts.barman.recover._restore_backup")
def test_run_barman_recover(self, mock_rb):
api = MagicMock()
args = MagicMock()
# successful execution
mock_rb.return_value = BarmanRecoverExitCode.RECOVERY_DONE
self.assertEqual(
run_barman_recover(api, args),
BarmanRecoverExitCode.RECOVERY_DONE,
)
mock_rb.assert_called_once_with(api, args.barman_server, args.backup_id,
args.ssh_command, args.data_directory,
args.loop_wait)
# failed execution
mock_rb.reset_mock()
mock_rb.return_value = BarmanRecoverExitCode.RECOVERY_FAILED
self.assertEqual(
run_barman_recover(api, args),
BarmanRecoverExitCode.RECOVERY_FAILED,
)
mock_rb.assert_called_once_with(api, args.barman_server, args.backup_id,
args.ssh_command, args.data_directory,
args.loop_wait)
# stuff from patroni.scripts.barman.config_switch
class TestBarmanConfigSwitch(unittest.TestCase):
def setUp(self):
self.api = MagicMock()
# Reset the mock as the same instance is used across tests
self.api._http.request.reset_mock()
self.api._http.request.side_effect = None
@patch("time.sleep")
@patch("logging.info")
@patch("logging.error")
def test__switch_config(self, mock_log_error, mock_log_info, mock_sleep):
mock_create_op = self.api.create_config_switch_operation
mock_get_status = self.api.get_operation_status
# successful fast config-switch
mock_create_op.return_value = "some_id"
mock_get_status.return_value = OperationStatus.DONE
self.assertEqual(
_switch_config(self.api, BARMAN_SERVER, BARMAN_MODEL, None),
BarmanConfigSwitchExitCode.CONFIG_SWITCH_DONE,
)
mock_create_op.assert_called_once_with(BARMAN_SERVER, BARMAN_MODEL, None)
mock_get_status.assert_called_once_with(BARMAN_SERVER, "some_id")
mock_log_info.assert_has_calls([
mock.call("Created the config switch operation with ID %s", "some_id"),
mock.call("Config switch operation finished successfully."),
])
mock_log_error.assert_not_called()
mock_sleep.assert_not_called()
# successful slow config-switch
mock_create_op.reset_mock()
mock_get_status.reset_mock()
mock_log_info.reset_mock()
mock_get_status.side_effect = [OperationStatus.IN_PROGRESS] * 20 + [OperationStatus.DONE]
self.assertEqual(
_switch_config(self.api, BARMAN_SERVER, BARMAN_MODEL, None),
BarmanConfigSwitchExitCode.CONFIG_SWITCH_DONE,
)
mock_create_op.assert_called_once_with(BARMAN_SERVER, BARMAN_MODEL, None)
self.assertEqual(mock_get_status.call_count, 21)
mock_get_status.assert_has_calls([mock.call(BARMAN_SERVER, "some_id")] * 21)
self.assertEqual(mock_log_info.call_count, 22)
mock_log_info.assert_has_calls([mock.call("Created the config switch operation with ID %s", "some_id")]
+ [mock.call("Config switch operation %s is still in progress", "some_id")] * 20
+ [mock.call("Config switch operation finished successfully.")])
mock_log_error.assert_not_called()
self.assertEqual(mock_sleep.call_count, 20)
mock_sleep.assert_has_calls([mock.call(5)] * 20)
# failed fast config-switch
mock_create_op.reset_mock()
mock_get_status.reset_mock()
mock_log_info.reset_mock()
mock_sleep.reset_mock()
mock_get_status.side_effect = None
mock_get_status.return_value = OperationStatus.FAILED
self.assertEqual(
_switch_config(self.api, BARMAN_SERVER, BARMAN_MODEL, None),
BarmanConfigSwitchExitCode.CONFIG_SWITCH_FAILED,
)
mock_create_op.assert_called_once()
mock_get_status.assert_called_once_with(BARMAN_SERVER, "some_id")
mock_log_info.assert_called_once_with("Created the config switch operation with ID %s", "some_id")
mock_log_error.assert_called_once_with("Config switch operation failed.")
mock_sleep.assert_not_called()
# failed slow config-switch
mock_create_op.reset_mock()
mock_get_status.reset_mock()
mock_log_info.reset_mock()
mock_log_error.reset_mock()
mock_sleep.reset_mock()
mock_get_status.side_effect = [OperationStatus.IN_PROGRESS] * 20 + [OperationStatus.FAILED]
self.assertEqual(
_switch_config(self.api, BARMAN_SERVER, BARMAN_MODEL, None),
BarmanConfigSwitchExitCode.CONFIG_SWITCH_FAILED,
)
mock_create_op.assert_called_once()
self.assertEqual(mock_get_status.call_count, 21)
mock_get_status.assert_has_calls([mock.call(BARMAN_SERVER, "some_id")] * 21)
self.assertEqual(mock_log_info.call_count, 21)
mock_log_info.assert_has_calls([mock.call("Created the config switch operation with ID %s", "some_id")]
+ [mock.call("Config switch operation %s is still in progress", "some_id")] * 20)
mock_log_error.assert_called_once_with("Config switch operation failed.")
self.assertEqual(mock_sleep.call_count, 20)
mock_sleep.assert_has_calls([mock.call(5)] * 20)
# create retries exceeded
mock_log_info.reset_mock()
mock_log_error.reset_mock()
mock_sleep.reset_mock()
mock_create_op.side_effect = RetriesExceeded()
mock_get_status.side_effect = None
self.assertEqual(
_switch_config(self.api, BARMAN_SERVER, BARMAN_MODEL, None),
BarmanConfigSwitchExitCode.HTTP_ERROR,
)
mock_log_info.assert_not_called()
mock_log_error.assert_called_once_with("An issue was faced while trying to create a config switch operation: "
"%r",
mock_create_op.side_effect)
mock_sleep.assert_not_called()
# get status retries exceeded
mock_create_op.reset_mock()
mock_create_op.side_effect = None
mock_log_error.reset_mock()
mock_get_status.side_effect = RetriesExceeded
self.assertEqual(
_switch_config(self.api, BARMAN_SERVER, BARMAN_MODEL, None),
BarmanConfigSwitchExitCode.HTTP_ERROR,
)
mock_log_info.assert_called_once_with("Created the config switch operation with ID %s", "some_id")
mock_log_error.assert_called_once_with("Maximum number of retries exceeded, exiting.")
mock_sleep.assert_not_called()
class TestBarmanConfigSwitchCli(unittest.TestCase):
def test__should_skip_switch(self):
args = MagicMock()
for role, switch_when, expected in [
("master", "promoted", False),
("master", "demoted", True),
("master", "always", False),
("primary", "promoted", False),
("primary", "demoted", True),
("primary", "always", False),
("promoted", "promoted", False),
("promoted", "demoted", True),
("promoted", "always", False),
("standby_leader", "promoted", True),
("standby_leader", "demoted", True),
("standby_leader", "always", False),
("replica", "promoted", True),
("replica", "demoted", False),
("replica", "always", False),
("demoted", "promoted", True),
("demoted", "demoted", False),
("demoted", "always", False),
]:
args.role = role
args.switch_when = switch_when
self.assertEqual(_should_skip_switch(args), expected)
@patch("patroni.scripts.barman.config_switch._should_skip_switch")
@patch("patroni.scripts.barman.config_switch._switch_config")
@patch("logging.error")
@patch("logging.info")
def test_run_barman_config_switch(self, mock_log_info, mock_log_error, mock_sc, mock_skip):
api = MagicMock()
args = MagicMock()
args.reset = None
# successful execution
mock_skip.return_value = False
mock_sc.return_value = BarmanConfigSwitchExitCode.CONFIG_SWITCH_DONE
self.assertEqual(
run_barman_config_switch(api, args),
BarmanConfigSwitchExitCode.CONFIG_SWITCH_DONE,
)
mock_sc.assert_called_once_with(api, args.barman_server, args.barman_model,
args.reset)
# failed execution
mock_sc.reset_mock()
mock_sc.return_value = BarmanConfigSwitchExitCode.CONFIG_SWITCH_FAILED
self.assertEqual(
run_barman_config_switch(api, args),
BarmanConfigSwitchExitCode.CONFIG_SWITCH_FAILED,
)
mock_sc.assert_called_once_with(api, args.barman_server, args.barman_model,
args.reset)
# skipped execution
mock_sc.reset_mock()
mock_skip.return_value = True
self.assertEqual(
run_barman_config_switch(api, args),
BarmanConfigSwitchExitCode.CONFIG_SWITCH_SKIPPED
)
mock_sc.assert_not_called()
mock_log_info.assert_called_once_with("Config switch operation was skipped (role=%s, "
"switch_when=%s).", args.role, args.switch_when)
mock_log_error.assert_not_called()
# invalid args -- sample 1
mock_skip.return_value = False
args = MagicMock()
args.barman_server = BARMAN_SERVER
args.barman_model = BARMAN_MODEL
args.reset = True
self.assertEqual(
run_barman_config_switch(api, args),
BarmanConfigSwitchExitCode.INVALID_ARGS,
)
mock_log_error.assert_called_once_with("One, and only one among 'barman_model' ('%s') and 'reset' "
"('%s') should be given", BARMAN_MODEL, True)
api.assert_not_called()
# invalid args -- sample 2
args = MagicMock()
args.barman_server = BARMAN_SERVER
args.barman_model = None
args.reset = None
mock_log_error.reset_mock()
api.reset_mock()
self.assertEqual(
run_barman_config_switch(api, args),
BarmanConfigSwitchExitCode.INVALID_ARGS,
)
mock_log_error.assert_called_once_with("One, and only one among 'barman_model' ('%s') and 'reset' "
"('%s') should be given", None, None)
api.assert_not_called()
# stuff from patroni.scripts.barman.cli
class TestMain(unittest.TestCase):
@patch("patroni.scripts.barman.cli.PgBackupApi")
@patch("patroni.scripts.barman.cli.set_up_logging")
@patch("patroni.scripts.barman.cli.ArgumentParser")
def test_main(self, mock_arg_parse, mock_set_up_log, mock_api):
# sub-command specified
args = MagicMock()
args.func.return_value = 0
mock_arg_parse.return_value.parse_known_args.return_value = (args, None)
with self.assertRaises(SystemExit) as exc:
main()
mock_arg_parse.assert_called_once()
mock_set_up_log.assert_called_once_with(args.log_file)
mock_api.assert_called_once_with(args.api_url, args.cert_file,
args.key_file, args.retry_wait,
args.max_retries)
mock_arg_parse.return_value.print_help.assert_not_called()
args.func.assert_called_once_with(mock_api.return_value, args)
self.assertEqual(exc.exception.code, 0)
# Issue in the API
mock_arg_parse.reset_mock()
mock_set_up_log.reset_mock()
mock_api.reset_mock()
mock_api.side_effect = ApiNotOk()
with self.assertRaises(SystemExit) as exc:
main()
mock_arg_parse.assert_called_once()
mock_set_up_log.assert_called_once_with(args.log_file)
mock_api.assert_called_once_with(args.api_url, args.cert_file,
args.key_file, args.retry_wait,
args.max_retries)
mock_arg_parse.return_value.print_help.assert_not_called()
self.assertEqual(exc.exception.code, -2)
# sub-command not specified
mock_arg_parse.reset_mock()
mock_set_up_log.reset_mock()
mock_api.reset_mock()
delattr(args, "func")
mock_api.side_effect = None
with self.assertRaises(SystemExit) as exc:
main()
mock_arg_parse.assert_called_once()
mock_set_up_log.assert_called_once_with(args.log_file)
mock_api.assert_not_called()
mock_arg_parse.return_value.print_help.assert_called_once_with()
self.assertEqual(exc.exception.code, -1)