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. - Implement unit tests.
""" """
import logging
from plinth import actions, action_utils, module_loader from plinth import actions, action_utils, module_loader
logger = logging.getLogger(__name__)
def validate(backup): def validate(backup):
"""Validate the backup' information schema.""" """Validate the backup' information schema."""
@ -76,6 +80,21 @@ def _validate_service(service):
assert service['kind'] in ('config', 'site', 'module') 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: class Packet:
"""Information passed to a handlers for backup/restore operations.""" """Information passed to a handlers for backup/restore operations."""
@ -97,6 +116,7 @@ class Packet:
self.root = root self.root = root
self.apps = apps self.apps = apps
self.label = label self.label = label
self.errors = []
self.directories = [] self.directories = []
self.files = [] self.files = []
@ -221,6 +241,19 @@ class BackupApp:
self.manifest == other_app.manifest and \ self.manifest == other_app.manifest and \
self.has_data == other_app.has_data 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(): def get_all_apps_for_backup():
"""Return a list of all applications that can be backed up.""" """Return a list of all applications that can be backed up."""
@ -411,6 +444,40 @@ def _restore_services(original_state):
service_handler.restart() 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): def _run_operation(handler, packet):
"""Run handler and pre/post hooks for backup/restore operations.""" """Run handler and pre/post hooks for backup/restore operations."""
_run_hooks(packet.operation + '_pre', packet)
handler(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))) 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): class TestBackupProcesses(unittest.TestCase):
"""Test cases for backup processes""" """Test cases for backup processes"""
@ -209,6 +230,21 @@ class TestBackupProcesses(unittest.TestCase):
] ]
run.assert_has_calls(calls) 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): class TestBackupModule(unittest.TestCase):
"""Tests of the backups django module, like views or forms.""" """Tests of the backups django module, like views or forms."""