Host Backup and Restore
Various setup and teardown hooks
called by pytest-mh can be used to implement automatic host backup and restore
functionality. This is supported out of the box with
MultihostBackupHost and
BackupTopologyController.
Implementing automatic backup of a host
MultihostBackupHost is an abstract class that declares
several abstract methods that have to be implemented:
Abstract method name |
Description |
|---|---|
Start required host services. If no services are needed, this can be
implemented as a “no operation” or raise |
|
Stop required host services. If no services are needed, this can be
implemented as a “no operation” or raise |
|
Take backup of the host. The backup can be returned as any Python
data, |
|
Restore the host from the backup. |
The backup is taken automatically during pytest setup, the host is restored
to this state after each test run. Sometimes, it is not desirable to restore the
host automatically at this point (for example if this is done by the topology
controller) and this can be disabled by passing auto_restore=False to the
constructor.
class ExampleBackupHost(MultihostBackupHost[MyProjectMultihostDomain]):
def __init__(self, *args, **kwargs) -> None:
# restore is handled in topology controllers
super().__init__(*args, auto_restore=False, **kwargs)
self.svc: SystemdServices = SystemdServices(self)
def start(self) -> None:
self.svc.start("my-project")
def stop(self) -> None:
self.svc.stop("my-project")
def backup(self) -> Any:
self.logger.info("Creating backup of my-project service")
# yields backup path
result = self.conn.run("my-project create-backup", log_level=ProcessLogLevel.Error)
return PurePosixPath(result.stdout_lines[-1].strip())
def restore(self, backup_data: Any | None) -> None:
if backup_data is None:
return
if not isinstance(backup_data, PurePosixPath):
raise TypeError(f"Expected PurePosixPath, got {type(backup_data)}")
backup_path = str(backup_data)
self.logger.info(f"Restoring my-project from {backup_path}")
self.stop()
self.conn.run(f"my-project restore {backup_path}", log_level=ProcessLogLevel.Error)
self.start()
Note
Some projects can not take online backups and the services must be stopped.
In such case, it is possible to pass auto_start=False to the constructor
to prevent automatic start up of the service before taking the first backup.
In this case, you must start the service manually when it is desired, for
example after the backup is taken or in
setup().
1class ExampleBackupHost(MultihostBackupHost[MyProjectMultihostDomain]):
2 def __init__(self, *args, **kwargs) -> None:
3 super().__init__(*args, auto_start=False, **kwargs)
4
5 self.svc: SystemdServices = SystemdServices(self)
6
7...
8
9def backup(self) -> Any:
10 self.logger.info("Creating backup of my-project service")
11
12 self.stop()
13 # yields backup path
14 result = self.conn.run("my-project create-backup", log_level=ProcessLogLevel.Error)
15 self.start()
16
17 return PurePosixPath(result.stdout_lines[-1].strip())
18
19...
Warning
Using reentrant utilities (instances of
MultihostReentrantUtility) inside
backup() and
restore() may not work as you might
expect. Remember that the reentrant utilities revert their actions during
teardown of the scope where they exist. However, backup and restore are
called from different scopes: backup()
is called from pytest_setup()
(per-session scope), but restore() is
called from teardown() (per-test
scope). It is therefore better to avoid them, unless you are sure that it
does what you want.
It is safer to use the SystemdServices
in the examples above, because the expected service state is started
after both backup and restore.
Implementing automatic backup for a topology
The previous section showed how to implement an automatic backup for each host. However, it is quite often the case that each host needs to get additional setup in order to prepare it for a given topology (like configuring the particular database backend that we want to test with this topology).
The topology controller provides various setup and teardown hooks that can setup the topology, take backup, restore to this backup after each test and when all tests for this topology are run, it can restore the hosts to their original state before the topology setup was run.
This behavior is implemented by the built-in
BackupTopologyController. This controller can be used as is
or further modified. Usually, it is desirable to override
topology_setup() to prepare the hosts
for testing. The automatic backup and restore is implemented only for the hosts
that inherits from MultihostBackupHost.
Warning
if BackupTopologyController is used, make sure to
disable automatic teardown in the hosts by passing auto_restore=False to
the MultihostBackupHost constructor.
class MyProjectTopologyController(BackupTopologyController[MyProjectMultihostConfig]):
@BackupTopologyController.restore_vanilla_on_error
def topology_setup(self, client: ClientHost, server: ServerHost) -> None:
self.logger.info(f"Preparing {server.hostname}")
# run your code
# Backup so we can restore to this state after each test
# There is no need to pass any arguments to this call
super().topology_setup()
Note
@BackupTopologyController.restore_vanilla_on_error decorator is used to
restore the hosts to the original state before topology setup was called if
any error occurs during the setup.