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:
Frode Nordahl 2019-09-25 17:11:45 +02:00
parent 4f7dba3da8
commit 574a0c2837
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
3 changed files with 163 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -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'),
])