Skip to content

Commit

Permalink
Add support for Barman instance operations
Browse files Browse the repository at this point in the history
So far we had only operations related with Barman servers in the
API:

* `RecoveryOperation`: to perform a `barman recover` of a Barman
  server
* `ConfigSwitchOperation`: to perform a `barman config-switch` to
  a Barman server

However, we intend to add an operation to the API which is performed
at the Barman instance level (global), not to a specific Barman
server: the `ConfigModelOperation`.

With that in mind this initial commit changes the classes below so
we will be able to create Barman instance operations:

* `OperationServer`: turn `server_name` argument optional. When it
  is `None`, that is considered an instance operation. Thus, the
  `job` and `output` files will be written under the Barman home
  in that case instead of under a Barman server directory
* `Operation`: turn `server_name` argument optional, so it creates
  `OperationServer` accrodingly

Unit tests changed accordingly, so we check both server and instance
operations.

Follow-up commits to come, which will implement new endpoints to the
API and the `ConfigModelOperation`.

References: BAR-126.

Signed-off-by: Israel Barth Rubio <[email protected]>
  • Loading branch information
barthisrael committed Jan 9, 2024
1 parent 896957b commit b12b68f
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 44 deletions.
74 changes: 44 additions & 30 deletions pg_backup_api/pg_backup_api/server_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,17 @@ class OperationNotExists(LookupError):

class OperationServer:
"""
Contain metadata about a Barman server and logic to handle operations.
Contain logic to handle operations for a Barman instance or Barman server.
:ivar name: name of the Barman server.
:ivar config: Barman configuration of the Barman server.
:ivar name: name of the Barman server, if it's server operation, otherwise
``None`` for a "global" (instance) operation.
:ivar config: Barman configuration of the Barman server, if it's a server
operation, otherwise ``None`` for a "global" (instance) operation.
:ivar jobs_basedir: directory where to save files of operations that have
been created for this Barman server.
been created for this Barman server or instance.
:ivar output_basedir: directory where to save files with output of
operations that have been finished for this Barman server -- both for
failed and successful executions.
operations that have been finished for this Barman server or instance
-- both for failed and successful executions.
"""

# Name of the pg-backup-api ``jobs`` directory. Files created under this
Expand All @@ -94,27 +96,33 @@ class OperationServer:
# Set of required keys when creating an operation output file.
_REQUIRED_OUTPUT_KEYS = ("success", "end_time", "output",)

def __init__(self, name: str) -> None:
def __init__(self, name: Optional[str]) -> None:
"""
Initialize a new instance of :class:`OperationServer`.
Fill all the metadata required by pg-backup-api of a given Barman
server named *name*, if it exists in Barman. Also prepare the Barman
server to execute pg-backup-api operations.
Fill all the metadata required by pg-backup-api for a given Barman
server named *name*, if a Barman server opartion and the server exists
in Barman. Also prepare the Barman server or instance to execute
pg-backup-api operations.
:param name: name of the Barman server.
:param name: name of the Barman server, if it's a Barman server
operation, ``None`` for a "global" (instance) operation.
:raises:
:exc:`OperationServerConfigError`: if no Barman configuration could
be found for server *name*.
be found for server *name*, in case of a Barman server
operation.
"""
self.name = name
self.config = get_server_by_name(name)
self.config = None

if not self.config:
raise OperationServerConfigError(
f"No barman config found for '{name}'."
)
if name:
self.config = get_server_by_name(name)

if not self.config:
raise OperationServerConfigError(
f"No barman config found for '{name}'."
)

load_barman_config()

Expand All @@ -123,10 +131,15 @@ def __init__(self, name: str) -> None:

barman_home = barman.__config__.barman_home

self.jobs_basedir = join(barman_home, name, self._JOBS_DIR_NAME)
self._create_jobs_dir()
if name:
self.jobs_basedir = join(barman_home, name, self._JOBS_DIR_NAME)
self.output_basedir = join(barman_home, name,
self._OUTPUT_DIR_NAME)
else:
self.jobs_basedir = join(barman_home, self._JOBS_DIR_NAME)
self.output_basedir = join(barman_home, self._OUTPUT_DIR_NAME)

self.output_basedir = join(barman_home, name, self._OUTPUT_DIR_NAME)
self._create_jobs_dir()
self._create_output_dir()

@staticmethod
Expand All @@ -151,11 +164,11 @@ def _create_dir(dir_path: str) -> None:
os.makedirs(dir_path)

def _create_jobs_dir(self) -> None:
"""Create the ``jobs`` directory of this Barman server."""
"""Create the ``jobs`` directory of Barman server or instance."""
self._create_dir(self.jobs_basedir)

def _create_output_dir(self) -> None:
"""Create the ``outputs`` directory of this Barman server."""
"""Create the ``outputs`` directory of Barman server or instance."""
self._create_dir(self.output_basedir)

def get_job_file_path(self, op_id: str) -> str:
Expand Down Expand Up @@ -318,16 +331,16 @@ def read_output_file(self, op_id: str) -> Dict[str, Any]:
def get_operations_list(self, op_type: Optional[OperationType] = None) \
-> List[Dict[str, Any]]:
"""
Get the list of operations of this Barman server.
Get the list of operations of this Barman server or instance.
Fetch operation from all ``.json`` files found under the
:attr:`jobs_basedir` of this server.
:attr:`jobs_basedir` of this server or instance.
:param op_type: if ``None`` retrieve all operations. If something other
than ``None``, filter by the given type.
:return: list of operations of this Barman server. Each item has the
following keys:
:return: list of operations of this Barman server or instance. Each
item has the following keys:
* ``id``: ID of the operation;
* ``type``: type of the operation.
Expand Down Expand Up @@ -393,12 +406,13 @@ class Operation:
:ivar id: ID of this operation.
"""

def __init__(self, server_name: str, id: Optional[str] = None) -> None:
def __init__(self, server_name: Optional[str],
id: Optional[str] = None) -> None:
"""
Initialize a new instance of :class:`Operation`.
:param server_name: name of the Barman server, so we can manage this
operation.
:param server_name: name of the Barman server, in case of a Barman
server operation, ``None`` in case of a Barman instance operation.
:param id: ID of the operation. Useful when querying an existing
operation. Use ``None`` when creating an operation, so this class
generates a new ID.
Expand Down Expand Up @@ -789,7 +803,7 @@ def main(callback: Callable[..., Any], *args: Tuple[Any, ...]) -> int:
"get information about jobs without a running REST API.",
)
parser.add_argument(
"--server-name", required=True,
"--server-name",
help="Name of the Barman server related to the operation.",
)
parser.add_argument(
Expand Down
38 changes: 24 additions & 14 deletions pg_backup_api/pg_backup_api/tests/test_server_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,44 @@
class TestOperationServer:
"""Run tests for :class:`OperationServer`."""

@pytest.fixture
@pytest.fixture(params=[_BARMAN_SERVER, None])
@patch("pg_backup_api.server_operation.get_server_by_name", Mock())
@patch("pg_backup_api.server_operation.load_barman_config", Mock())
@patch.object(OperationServer, "_create_dir", Mock())
def op_server(self):
def op_server(self, request):
"""Create a :class:`OperationServer` instance for testing.
:return: :class:`OperationServer` instance for testing.
"""
with patch("barman.__config__") as mock_config:
mock_config.barman_home = _BARMAN_HOME
return OperationServer(_BARMAN_SERVER)
return OperationServer(request.param)

def test___init__(self, op_server):
"""Test :meth:`OperationServer.__init__`.
Ensure its attributes are set as expected.
"""
# Handle the 2 possible fixtures, one for server operations and another
# for instance operations
expected_name = None
expected_jobs = os.path.join(_BARMAN_HOME, "jobs")
expected_output = os.path.join(_BARMAN_HOME, "output")

if op_server.name is not None:
expected_name = _BARMAN_SERVER
expected_jobs = os.path.join(_BARMAN_HOME, _BARMAN_SERVER, "jobs")
expected_output = os.path.join(_BARMAN_HOME, _BARMAN_SERVER,
"output")

# Ensure name is as expected.
assert op_server.name == _BARMAN_SERVER
assert op_server.name == expected_name

# Ensure "jobs" directory is created in expected path.
expected = os.path.join(_BARMAN_HOME, _BARMAN_SERVER, "jobs")
assert op_server.jobs_basedir == expected
assert op_server.jobs_basedir == expected_jobs

# Ensure "output" directory is created in the expected path.
expected = os.path.join(_BARMAN_HOME, _BARMAN_SERVER, "output")
assert op_server.output_basedir == expected
assert op_server.output_basedir == expected_output

@patch("os.path.isdir")
@patch("os.path.exists")
Expand Down Expand Up @@ -586,14 +596,14 @@ def test_get_operation_status_exception(self, mock_read_job_file,
class TestOperation:
"""Run tests for :class:`Operation`."""

@pytest.fixture
@patch("pg_backup_api.server_operation.OperationServer", MagicMock())
def operation(self):
@pytest.fixture(params=[_BARMAN_SERVER, None])
@patch("pg_backup_api.server_operation.OperationServer")
def operation(self, mock_op_server, request):
"""Create an :class:`Operation` instance for testing.
:return: a new instance of :class:`Operation` for testing.
"""
return Operation(_BARMAN_SERVER)
return Operation(request.param)

def test___init___auto_id(self, operation):
"""Test :meth:`Operation.__init__`.
Expand All @@ -604,7 +614,7 @@ def test___init___auto_id(self, operation):

with patch.object(Operation, "_generate_id") as mock_generate_id:
mock_generate_id.return_value = id
operation = Operation(_BARMAN_SERVER)
operation = Operation(operation.server.name)
assert operation.id == id
mock_generate_id.assert_called_once()

Expand All @@ -616,7 +626,7 @@ def test___init___custom_id(self, operation):
id = "CUSTOM_OP_ID"

with patch.object(Operation, "_generate_id") as mock_generate_id:
operation = Operation(_BARMAN_SERVER, id)
operation = Operation(operation.server.name, id)
assert operation.id == id
mock_generate_id.assert_not_called()

Expand Down

0 comments on commit b12b68f

Please sign in to comment.