mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
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:
parent
2c1372c26d
commit
316d765629
@ -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)
|
||||||
|
|||||||
@ -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."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user