backups: Implement app hooks

Each application will be able to implement backup_pre, backup_post, restore_pre
and restore_post hooks that get called before/after backup/restore
appropriately. This is to handle any edge cases that backup manifest mechanism
does not handle.

Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2018-10-18 23:16:23 -07:00 committed by James Valleroy
parent 2c1372c26d
commit 316d765629
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
2 changed files with 103 additions and 0 deletions

View File

@ -25,8 +25,12 @@ TODO:
- Implement unit tests.
"""
import logging
from plinth import actions, action_utils, module_loader
logger = logging.getLogger(__name__)
def validate(backup):
"""Validate the backup' information schema."""
@ -76,6 +80,21 @@ def _validate_service(service):
assert service['kind'] in ('config', 'site', 'module')
class BackupError:
"""Represent an backup/restore operation error."""
def __init__(self, error_type, app, hook=None):
"""Initialize the error object."""
self.error_type = error_type
self.app = app
self.hook = hook
def __eq__(self, other_error):
"""Compare to error objects."""
return (self.error_type == other_error.error_type and
self.app == other_error.app and
self.hook == other_error.hook)
class Packet:
"""Information passed to a handlers for backup/restore operations."""
@ -97,6 +116,7 @@ class Packet:
self.root = root
self.apps = apps
self.label = label
self.errors = []
self.directories = []
self.files = []
@ -221,6 +241,19 @@ class BackupApp:
self.manifest == other_app.manifest and \
self.has_data == other_app.has_data
def run_hook(self, hook, packet):
"""Run a hook inside an application."""
if not hasattr(self.app, hook):
return
try:
getattr(self.app, hook)(packet)
except Exception as exception:
logger.exception(
'Error running backup/restore hook for app %s: %s', self.name,
exception)
packet.errors.append(BackupError('hook', self.app, hook=hook))
def get_all_apps_for_backup():
"""Return a list of all applications that can be backed up."""
@ -411,6 +444,40 @@ def _restore_services(original_state):
service_handler.restart()
def _run_hooks(hook, packet):
"""Run pre/post operation hooks in applications.
Using the manifest mechanism, applications will convey to the backups
framework how they needs to be backed up. Using this declarative approach
reduces the burden of implementation on behalf of the applications.
However, not all backup necessities may be satisfied in this manner no
matter how feature rich the framework. So, applications should have the
ability to customize the backup/restore processes suiting to their needs.
For this, each application may optionally implement methods (hooks) that
will be called during the backup or restore process. If these methods are
named appropriately, the backups API will automatically call the methods
and there is no need to register the methods.
The following hooks are currently available for implementation:
- backup_pre(packet):
Called before the backup process starts for the application.
- backup_post(packet):
Called after the backup process has completed for the application.
- restore_pre(packet):
Called before the restore process starts for the application.
- restore_post(packet):
Called after the restore process has completed for the application.
"""
logger.info('Running %s hooks', hook)
for app in packet.apps:
app.run_hook(hook, packet)
def _run_operation(handler, packet):
"""Run handler and pre/post hooks for backup/restore operations."""
_run_hooks(packet.operation + '_pre', packet)
handler(packet)
_run_hooks(packet.operation + '_post', packet)

View File

@ -58,6 +58,27 @@ def _get_backup_app(name):
return api.BackupApp(name, MagicMock(backup=_get_test_manifest(name)))
class TestBackupApp(unittest.TestCase):
"""Test the BackupApp class."""
def test_run_hook(self):
"""Test running a hook on an application."""
packet = api.Packet('backup', 'apps', '/', [])
hook = 'testhook_pre'
app = MagicMock()
backup_app = api.BackupApp('app_name', app)
backup_app.run_hook(hook, packet)
app.testhook_pre.assert_has_calls([call(packet)])
assert not packet.errors
app.testhook_pre.reset_mock()
app.testhook_pre.side_effect = Exception()
backup_app.run_hook(hook, packet)
self.assertEqual(packet.errors, [api.BackupError('hook', app, hook=hook)])
del app.testhook_pre
backup_app.run_hook(hook, packet)
class TestBackupProcesses(unittest.TestCase):
"""Test cases for backup processes"""
@ -209,6 +230,21 @@ class TestBackupProcesses(unittest.TestCase):
]
run.assert_has_calls(calls)
@staticmethod
def test__run_operation():
"""Test that operation runs handler and app hooks."""
apps = [_get_backup_app('a'), _get_backup_app('b')]
packet = api.Packet('backup', 'apps', '/', apps)
packet.apps[0].run_hook = MagicMock()
packet.apps[1].run_hook = MagicMock()
handler = MagicMock()
api._run_operation(handler, packet)
handler.assert_has_calls([call(packet)])
calls = [call('backup_pre', packet), call('backup_post', packet)]
packet.apps[0].run_hook.assert_has_calls(calls)
packet.apps[1].run_hook.assert_has_calls(calls)
class TestBackupModule(unittest.TestCase):
"""Tests of the backups django module, like views or forms."""