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
|
# Overview
|
||||||
|
|
||||||
This interface is used for a charm to send configuration information to the
|
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
|
neutron-api principle charm, request a restart of a service managed by
|
||||||
that charm.
|
that charm, and request database migration to be performed.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
@ -10,6 +10,10 @@ that charm.
|
|||||||
The interface provides the `{relation-name}.connected` and
|
The interface provides the `{relation-name}.connected` and
|
||||||
`{relation_name}.available` flags and states.
|
`{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
|
## neutron\_config\_data
|
||||||
|
|
||||||
The neutron\_config\_data property allows the charm author to introspect a
|
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')
|
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
|
# Metadata
|
||||||
|
|
||||||
To consume this interface in your charm or layer, add the following to
|
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 uuid
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import charms.reactive as reactive
|
||||||
|
|
||||||
from charms.reactive import hook
|
from charms.reactive import hook
|
||||||
from charms.reactive import RelationBase
|
from charms.reactive import RelationBase
|
||||||
from charms.reactive import scopes
|
from charms.reactive import scopes
|
||||||
@ -12,9 +14,9 @@ class NeutronPluginAPISubordinate(RelationBase):
|
|||||||
@hook(
|
@hook(
|
||||||
'{provides:neutron-plugin-api-subordinate}-relation-{joined,changed}')
|
'{provides:neutron-plugin-api-subordinate}-relation-{joined,changed}')
|
||||||
def changed(self):
|
def changed(self):
|
||||||
"""Set connected state"""
|
"""Set connected state and assess available state"""
|
||||||
self.set_state('{relation_name}.connected')
|
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')
|
self.set_state('{relation_name}.available')
|
||||||
|
|
||||||
@hook(
|
@hook(
|
||||||
@ -26,9 +28,11 @@ class NeutronPluginAPISubordinate(RelationBase):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def neutron_config_data(self):
|
def neutron_config_data(self):
|
||||||
|
"""Retrive and decode ``neutron_config_data`` from relation"""
|
||||||
return json.loads(self.get_remote('neutron_config_data', "{}"))
|
return json.loads(self.get_remote('neutron_config_data', "{}"))
|
||||||
|
|
||||||
def neutron_api_ready(self):
|
def neutron_api_ready(self):
|
||||||
|
"""Assess remote readiness"""
|
||||||
if self.get_remote('neutron-api-ready') == 'yes':
|
if self.get_remote('neutron-api-ready') == 'yes':
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -88,7 +92,6 @@ class NeutronPluginAPISubordinate(RelationBase):
|
|||||||
"""
|
"""
|
||||||
if subordinate_configuration is None:
|
if subordinate_configuration is None:
|
||||||
subordinate_configuration = {}
|
subordinate_configuration = {}
|
||||||
conversation = self.conversation()
|
|
||||||
relation_info = {
|
relation_info = {
|
||||||
'neutron-plugin': neutron_plugin,
|
'neutron-plugin': neutron_plugin,
|
||||||
'core-plugin': core_plugin,
|
'core-plugin': core_plugin,
|
||||||
@ -100,7 +103,7 @@ class NeutronPluginAPISubordinate(RelationBase):
|
|||||||
'neutron-security-groups': neutron_security_groups,
|
'neutron-security-groups': neutron_security_groups,
|
||||||
'subordinate_configuration': json.dumps(subordinate_configuration),
|
'subordinate_configuration': json.dumps(subordinate_configuration),
|
||||||
}
|
}
|
||||||
conversation.set_remote(**relation_info)
|
self.set_remote(**relation_info)
|
||||||
|
|
||||||
def request_restart(self, service_type=None):
|
def request_restart(self, service_type=None):
|
||||||
"""Request a restart of a set of remote services
|
"""Request a restart of a set of remote services
|
||||||
@ -117,3 +120,52 @@ class NeutronPluginAPISubordinate(RelationBase):
|
|||||||
key: str(uuid.uuid4()),
|
key: str(uuid.uuid4()),
|
||||||
}
|
}
|
||||||
self.set_remote(**relation_info)
|
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'})
|
self.assertEquals(self.target.neutron_config_data, {'k': 'v'})
|
||||||
|
|
||||||
def test_configure_plugin(self):
|
def test_configure_plugin(self):
|
||||||
conversation = mock.MagicMock()
|
self.patch_target('set_remote')
|
||||||
self.patch_target('conversation', conversation)
|
|
||||||
self.target.configure_plugin('aPlugin',
|
self.target.configure_plugin('aPlugin',
|
||||||
'aCorePlugin',
|
'aCorePlugin',
|
||||||
'aNeutronPluginConfig',
|
'aNeutronPluginConfig',
|
||||||
@ -156,7 +155,7 @@ class TestNeutronPluginApiSubordinateProvides(test_utils.PatchHelper):
|
|||||||
'typeDriver1,typeDriver2',
|
'typeDriver1,typeDriver2',
|
||||||
'toggleSecurityGroups',
|
'toggleSecurityGroups',
|
||||||
)
|
)
|
||||||
conversation.set_remote.assert_called_once_with(
|
self.set_remote.assert_called_once_with(
|
||||||
**{
|
**{
|
||||||
'core-plugin': 'aCorePlugin',
|
'core-plugin': 'aCorePlugin',
|
||||||
'neutron-plugin': 'aPlugin',
|
'neutron-plugin': 'aPlugin',
|
||||||
@ -170,16 +169,63 @@ class TestNeutronPluginApiSubordinateProvides(test_utils.PatchHelper):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_request_restart(self):
|
def test_request_restart(self):
|
||||||
conversation = mock.MagicMock()
|
|
||||||
self.patch_target('conversation', conversation)
|
|
||||||
self.patch_object(provides.uuid, 'uuid4')
|
self.patch_object(provides.uuid, 'uuid4')
|
||||||
self.uuid4.return_value = 'fake-uuid'
|
self.uuid4.return_value = 'fake-uuid'
|
||||||
|
self.patch_target('set_remote')
|
||||||
self.target.request_restart()
|
self.target.request_restart()
|
||||||
conversation.set_remote.assert_called_once_with(
|
self.set_remote.assert_called_once_with(
|
||||||
None, None, None, **{'restart-trigger': 'fake-uuid'},
|
**{'restart-trigger': 'fake-uuid'},
|
||||||
)
|
)
|
||||||
conversation.set_remote.reset_mock()
|
self.set_remote.reset_mock()
|
||||||
self.target.request_restart('aServiceType')
|
self.target.request_restart('aServiceType')
|
||||||
conversation.set_remote.assert_called_once_with(
|
self.set_remote.assert_called_once_with(
|
||||||
None, None, None, **{'restart-trigger-aServiceType': 'fake-uuid'},
|
**{'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…
x
Reference in New Issue
Block a user