Support database migrations
A charm may request principal to perform a DB migration. The charm may also optionally flag that interface availability should be gated on the completion of the requested DB migration. Change-Id: Id9ed69880f06b835ff2385721d4cb22e47f37c5d Depends-On: Iaefcb81fff5ed8a9441c93ac4c8bac3fa12eef15
This commit is contained in:
parent
4f7dba3da8
commit
574a0c2837
53
README.md
53
README.md
@ -1,8 +1,8 @@
|
||||
# Overview
|
||||
|
||||
This interface is used for a charm to send configuration information to the
|
||||
neutron-api principle charm and request a restart of a service managed by
|
||||
that charm.
|
||||
neutron-api principle charm, request a restart of a service managed by
|
||||
that charm, and request database migration to be performed.
|
||||
|
||||
# Usage
|
||||
|
||||
@ -10,6 +10,10 @@ that charm.
|
||||
The interface provides the `{relation-name}.connected` and
|
||||
`{relation_name}.available` flags and states.
|
||||
|
||||
The charm may set the `{relation-name}.db_migration` flag to instruct the
|
||||
interface code to gate the `{relation_name}.available` flag/state on
|
||||
completion of any in-flight database migration requests.
|
||||
|
||||
## neutron\_config\_data
|
||||
|
||||
The neutron\_config\_data property allows the charm author to introspect a
|
||||
@ -96,6 +100,51 @@ def remote_restart(api_principle):
|
||||
api_principle.request_restart(service_type='neutron')
|
||||
```
|
||||
|
||||
## request\_db\_migration
|
||||
|
||||
Request principle charm to perform a DB migration. This is useful both at
|
||||
initial deploy time and at subsequent changes as the lifecycle of the
|
||||
subordinate may be independent of the principle charm.
|
||||
|
||||
An example of how to request db migration upon initial deployment:
|
||||
|
||||
```python
|
||||
@reactive.when_none('neutron-plugin-api-subordinate.db_migration',
|
||||
'neutron-plugin-api-subordinate.available')
|
||||
@reactive.when('charm.installed')
|
||||
def flag_db_migration():
|
||||
reactive.set_flag('neutron-plugin-api-subordinate.db_migration')
|
||||
|
||||
|
||||
@reactive.when_none('neutron-plugin-api-subordinate.available',
|
||||
'run-default-update-status')
|
||||
@reactive.when('neutron-plugin-api-subordinate.connected')
|
||||
def request_db_migration():
|
||||
neutron = reactive.endpoint_from_flag(
|
||||
'neutron-plugin-api-subordinate.connected')
|
||||
neutron.request_db_migration()
|
||||
```
|
||||
|
||||
An example of usage in conjunction with post deployment change:
|
||||
|
||||
```python
|
||||
@reactive.when('config.changed')
|
||||
def handle_change():
|
||||
...
|
||||
if config_change_added_package_which_requires_db_migration:
|
||||
neutron = reactive.endpoint_from_flag(
|
||||
'neutron-plugin-api-subordinate.connected')
|
||||
neutron.request_db_migration()
|
||||
|
||||
|
||||
@reactive.when('neutron-plugin-api-subordinate.available')
|
||||
def do_something():
|
||||
...
|
||||
# After requesting the DB migration above, you will not get here until it
|
||||
# is done.
|
||||
use_new_feature()
|
||||
```
|
||||
|
||||
# Metadata
|
||||
|
||||
To consume this interface in your charm or layer, add the following to
|
||||
|
60
provides.py
60
provides.py
@ -1,6 +1,8 @@
|
||||
import uuid
|
||||
import json
|
||||
|
||||
import charms.reactive as reactive
|
||||
|
||||
from charms.reactive import hook
|
||||
from charms.reactive import RelationBase
|
||||
from charms.reactive import scopes
|
||||
@ -12,9 +14,9 @@ class NeutronPluginAPISubordinate(RelationBase):
|
||||
@hook(
|
||||
'{provides:neutron-plugin-api-subordinate}-relation-{joined,changed}')
|
||||
def changed(self):
|
||||
"""Set connected state"""
|
||||
"""Set connected state and assess available state"""
|
||||
self.set_state('{relation_name}.connected')
|
||||
if self.neutron_api_ready():
|
||||
if self.neutron_api_ready() and not self.db_migration_pending():
|
||||
self.set_state('{relation_name}.available')
|
||||
|
||||
@hook(
|
||||
@ -26,9 +28,11 @@ class NeutronPluginAPISubordinate(RelationBase):
|
||||
|
||||
@property
|
||||
def neutron_config_data(self):
|
||||
"""Retrive and decode ``neutron_config_data`` from relation"""
|
||||
return json.loads(self.get_remote('neutron_config_data', "{}"))
|
||||
|
||||
def neutron_api_ready(self):
|
||||
"""Assess remote readiness"""
|
||||
if self.get_remote('neutron-api-ready') == 'yes':
|
||||
return True
|
||||
return False
|
||||
@ -88,7 +92,6 @@ class NeutronPluginAPISubordinate(RelationBase):
|
||||
"""
|
||||
if subordinate_configuration is None:
|
||||
subordinate_configuration = {}
|
||||
conversation = self.conversation()
|
||||
relation_info = {
|
||||
'neutron-plugin': neutron_plugin,
|
||||
'core-plugin': core_plugin,
|
||||
@ -100,7 +103,7 @@ class NeutronPluginAPISubordinate(RelationBase):
|
||||
'neutron-security-groups': neutron_security_groups,
|
||||
'subordinate_configuration': json.dumps(subordinate_configuration),
|
||||
}
|
||||
conversation.set_remote(**relation_info)
|
||||
self.set_remote(**relation_info)
|
||||
|
||||
def request_restart(self, service_type=None):
|
||||
"""Request a restart of a set of remote services
|
||||
@ -117,3 +120,52 @@ class NeutronPluginAPISubordinate(RelationBase):
|
||||
key: str(uuid.uuid4()),
|
||||
}
|
||||
self.set_remote(**relation_info)
|
||||
|
||||
def request_db_migration(self):
|
||||
"""Request principal to perform a DB migration"""
|
||||
if not self.neutron_api_ready():
|
||||
# Ignore the charm request until we are in a relation-changed hook
|
||||
# where the prinicpal charm has declared itself ready.
|
||||
return
|
||||
nonce = str(uuid.uuid4())
|
||||
relation_info = {
|
||||
'migrate-database-nonce': nonce,
|
||||
}
|
||||
self.set_remote(**relation_info)
|
||||
# NOTE: we use flags instead of RelationBase state here both because of
|
||||
# easier interaction with charm code, and because of how states
|
||||
# interact with RelationBase conversations leading to crashes
|
||||
# when used prior to relation being fully established.
|
||||
reactive.set_flag('{relation_name}.db_migration'
|
||||
.format(relation_name=self.relation_name))
|
||||
reactive.set_flag('{relation_name}.db_migration.'
|
||||
.format(relation_name=self.relation_name)+nonce)
|
||||
reactive.clear_flag('{relation_name}.available'
|
||||
.format(relation_name=self.relation_name))
|
||||
|
||||
def db_migration_pending(self):
|
||||
"""Assess presence and state of optional DB migration request"""
|
||||
# NOTE: we use flags instead of RelationBase state here both because of
|
||||
# easier interaction with charm code, and because of how states
|
||||
# interact with RelationBase conversations leading to crashes
|
||||
# when used prior to relation being fully established.
|
||||
flag_prefix = ('{relation_name}.db_migration'
|
||||
.format(relation_name=self.relation_name))
|
||||
if not reactive.is_flag_set(flag_prefix):
|
||||
return False
|
||||
flag_nonce = '.'.join(
|
||||
(flag_prefix,
|
||||
self.get_remote('migrate-database-nonce', '')))
|
||||
if reactive.is_flag_set(flag_nonce):
|
||||
# hooks fire in a nondeterministic order, and there will be
|
||||
# occations where a different hook run between the
|
||||
# ``migrate-database-nonce`` being set and it being returned to us
|
||||
# a subsequent relation-changed hook.
|
||||
#
|
||||
# to avoid buildup of unreaped db_migration nonce flags we remove
|
||||
# all of them each time we have a match for one.
|
||||
for flag in reactive.get_flags():
|
||||
if flag.startswith(flag_prefix):
|
||||
reactive.clear_flag(flag)
|
||||
return False
|
||||
return True
|
||||
|
@ -144,8 +144,7 @@ class TestNeutronPluginApiSubordinateProvides(test_utils.PatchHelper):
|
||||
self.assertEquals(self.target.neutron_config_data, {'k': 'v'})
|
||||
|
||||
def test_configure_plugin(self):
|
||||
conversation = mock.MagicMock()
|
||||
self.patch_target('conversation', conversation)
|
||||
self.patch_target('set_remote')
|
||||
self.target.configure_plugin('aPlugin',
|
||||
'aCorePlugin',
|
||||
'aNeutronPluginConfig',
|
||||
@ -156,7 +155,7 @@ class TestNeutronPluginApiSubordinateProvides(test_utils.PatchHelper):
|
||||
'typeDriver1,typeDriver2',
|
||||
'toggleSecurityGroups',
|
||||
)
|
||||
conversation.set_remote.assert_called_once_with(
|
||||
self.set_remote.assert_called_once_with(
|
||||
**{
|
||||
'core-plugin': 'aCorePlugin',
|
||||
'neutron-plugin': 'aPlugin',
|
||||
@ -170,16 +169,63 @@ class TestNeutronPluginApiSubordinateProvides(test_utils.PatchHelper):
|
||||
)
|
||||
|
||||
def test_request_restart(self):
|
||||
conversation = mock.MagicMock()
|
||||
self.patch_target('conversation', conversation)
|
||||
self.patch_object(provides.uuid, 'uuid4')
|
||||
self.uuid4.return_value = 'fake-uuid'
|
||||
self.patch_target('set_remote')
|
||||
self.target.request_restart()
|
||||
conversation.set_remote.assert_called_once_with(
|
||||
None, None, None, **{'restart-trigger': 'fake-uuid'},
|
||||
self.set_remote.assert_called_once_with(
|
||||
**{'restart-trigger': 'fake-uuid'},
|
||||
)
|
||||
conversation.set_remote.reset_mock()
|
||||
self.set_remote.reset_mock()
|
||||
self.target.request_restart('aServiceType')
|
||||
conversation.set_remote.assert_called_once_with(
|
||||
None, None, None, **{'restart-trigger-aServiceType': 'fake-uuid'},
|
||||
self.set_remote.assert_called_once_with(
|
||||
**{'restart-trigger-aServiceType': 'fake-uuid'},
|
||||
)
|
||||
|
||||
def test_request_db_migration(self):
|
||||
self.patch_target('neutron_api_ready')
|
||||
self.patch_object(provides.uuid, 'uuid4')
|
||||
self.neutron_api_ready.return_value = False
|
||||
self.target.request_db_migration()
|
||||
self.assertFalse(self.uuid4.called)
|
||||
self.patch_target('set_remote')
|
||||
self.patch_object(provides.reactive, 'set_flag')
|
||||
self.patch_object(provides.reactive, 'clear_flag')
|
||||
self.neutron_api_ready.return_value = True
|
||||
self.uuid4.return_value = 'fake-uuid'
|
||||
self.target.request_db_migration()
|
||||
self.set_remote.assert_called_once_with(
|
||||
**{'migrate-database-nonce': 'fake-uuid'})
|
||||
self.set_flag.assert_has_calls([
|
||||
mock.call('some-relation.db_migration'),
|
||||
mock.call('some-relation.db_migration.fake-uuid'),
|
||||
])
|
||||
self.clear_flag.assert_called_once_with('some-relation.available')
|
||||
|
||||
def test_db_migration_pending(self):
|
||||
self.patch_object(provides.reactive, 'is_flag_set')
|
||||
self.patch_target('get_remote')
|
||||
self.is_flag_set.return_value = False
|
||||
self.target.db_migration_pending()
|
||||
self.is_flag_set.assert_called_once_with('some-relation.db_migration')
|
||||
self.assertFalse(self.get_remote.called)
|
||||
self.is_flag_set.side_effect = [True, False]
|
||||
self.get_remote.return_value = 'fake-uuid'
|
||||
self.assertTrue(self.target.db_migration_pending())
|
||||
self.get_remote.assert_called_once_with('migrate-database-nonce', '')
|
||||
self.is_flag_set.assert_has_calls([
|
||||
mock.call('some-relation.db_migration'),
|
||||
mock.call('some-relation.db_migration.fake-uuid'),
|
||||
])
|
||||
self.is_flag_set.side_effect = [True, True]
|
||||
self.patch_object(provides.reactive, 'clear_flag')
|
||||
self.patch_object(provides.reactive, 'get_flags')
|
||||
self.get_flags.return_value = [
|
||||
'some-relation.db_migration.fake-uuid',
|
||||
'some-relation.db_migration',
|
||||
]
|
||||
self.assertFalse(self.target.db_migration_pending())
|
||||
self.clear_flag.assert_has_calls([
|
||||
mock.call('some-relation.db_migration.fake-uuid'),
|
||||
mock.call('some-relation.db_migration'),
|
||||
])
|
||||
|
Loading…
Reference in New Issue
Block a user