[thedac,r=james-page] Add support for action managed openstack upgrades.
Also fix misc assert_called problems in unit tests.
This commit is contained in:
commit
7fae6a199c
@ -1,2 +1,4 @@
|
||||
git-reinstall:
|
||||
description: Reinstall nova-cloud-controller from the openstack-origin-git repositories.
|
||||
openstack-upgrade:
|
||||
description: Perform openstack upgrades. Config option action-managed-upgrade must be set to True.
|
||||
|
1
actions/openstack-upgrade
Symbolic link
1
actions/openstack-upgrade
Symbolic link
@ -0,0 +1 @@
|
||||
openstack_upgrade.py
|
41
actions/openstack_upgrade.py
Executable file
41
actions/openstack_upgrade.py
Executable file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
|
||||
sys.path.append('hooks/')
|
||||
|
||||
from charmhelpers.contrib.openstack.utils import (
|
||||
do_action_openstack_upgrade,
|
||||
)
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
relation_ids,
|
||||
)
|
||||
|
||||
from nova_cc_utils import (
|
||||
do_openstack_upgrade,
|
||||
)
|
||||
|
||||
from nova_cc_hooks import (
|
||||
config_changed,
|
||||
CONFIGS,
|
||||
neutron_api_relation_joined,
|
||||
)
|
||||
|
||||
|
||||
def openstack_upgrade():
|
||||
"""Upgrade packages to config-set Openstack version.
|
||||
|
||||
If the charm was installed from source we cannot upgrade it.
|
||||
For backwards compatibility a config flag must be set for this
|
||||
code to run, otherwise a full service level upgrade will fire
|
||||
on config-changed."""
|
||||
|
||||
if (do_action_openstack_upgrade('nova-common',
|
||||
do_openstack_upgrade,
|
||||
CONFIGS)):
|
||||
[neutron_api_relation_joined(rid=rid, remote_restart=True)
|
||||
for rid in relation_ids('neutron-api')]
|
||||
config_changed()
|
||||
|
||||
if __name__ == '__main__':
|
||||
openstack_upgrade()
|
10
config.yaml
10
config.yaml
@ -395,3 +395,13 @@ options:
|
||||
|
||||
If memcached is being used to store the tokens, then it's recommended to
|
||||
change this configuration to False.
|
||||
action-managed-upgrade:
|
||||
type: boolean
|
||||
default: False
|
||||
description: |
|
||||
If True enables openstack upgrades for this charm via juju actions.
|
||||
You will still need to set openstack-origin to the new repository but
|
||||
instead of an upgrade running automatically across all units, it will
|
||||
wait for you to execute the openstack-upgrade action for this charm on
|
||||
each unit. If False it will revert to existing behavior of upgrading
|
||||
all units on config change.
|
||||
|
@ -42,9 +42,7 @@ from charmhelpers.core.hookenv import (
|
||||
charm_dir,
|
||||
INFO,
|
||||
relation_ids,
|
||||
relation_set,
|
||||
status_set,
|
||||
hook_name
|
||||
relation_set
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.storage.linux.lvm import (
|
||||
@ -148,6 +146,9 @@ PACKAGE_CODENAMES = {
|
||||
'glance-common': OrderedDict([
|
||||
('11.0.0', 'liberty'),
|
||||
]),
|
||||
'openstack-dashboard': OrderedDict([
|
||||
('8.0.0', 'liberty'),
|
||||
]),
|
||||
}
|
||||
|
||||
DEFAULT_LOOPBACK_SIZE = '5G'
|
||||
@ -753,176 +754,6 @@ def git_yaml_value(projects_yaml, key):
|
||||
return None
|
||||
|
||||
|
||||
def os_workload_status(configs, required_interfaces, charm_func=None):
|
||||
"""
|
||||
Decorator to set workload status based on complete contexts
|
||||
"""
|
||||
def wrap(f):
|
||||
@wraps(f)
|
||||
def wrapped_f(*args, **kwargs):
|
||||
# Run the original function first
|
||||
f(*args, **kwargs)
|
||||
# Set workload status now that contexts have been
|
||||
# acted on
|
||||
set_os_workload_status(configs, required_interfaces, charm_func)
|
||||
return wrapped_f
|
||||
return wrap
|
||||
|
||||
|
||||
def set_os_workload_status(configs, required_interfaces, charm_func=None):
|
||||
"""
|
||||
Set workload status based on complete contexts.
|
||||
status-set missing or incomplete contexts
|
||||
and juju-log details of missing required data.
|
||||
charm_func is a charm specific function to run checking
|
||||
for charm specific requirements such as a VIP setting.
|
||||
"""
|
||||
incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
|
||||
state = 'active'
|
||||
missing_relations = []
|
||||
incomplete_relations = []
|
||||
message = None
|
||||
charm_state = None
|
||||
charm_message = None
|
||||
|
||||
for generic_interface in incomplete_rel_data.keys():
|
||||
related_interface = None
|
||||
missing_data = {}
|
||||
# Related or not?
|
||||
for interface in incomplete_rel_data[generic_interface]:
|
||||
if incomplete_rel_data[generic_interface][interface].get('related'):
|
||||
related_interface = interface
|
||||
missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
|
||||
# No relation ID for the generic_interface
|
||||
if not related_interface:
|
||||
juju_log("{} relation is missing and must be related for "
|
||||
"functionality. ".format(generic_interface), 'WARN')
|
||||
state = 'blocked'
|
||||
if generic_interface not in missing_relations:
|
||||
missing_relations.append(generic_interface)
|
||||
else:
|
||||
# Relation ID exists but no related unit
|
||||
if not missing_data:
|
||||
# Edge case relation ID exists but departing
|
||||
if ('departed' in hook_name() or 'broken' in hook_name()) \
|
||||
and related_interface in hook_name():
|
||||
state = 'blocked'
|
||||
if generic_interface not in missing_relations:
|
||||
missing_relations.append(generic_interface)
|
||||
juju_log("{} relation's interface, {}, "
|
||||
"relationship is departed or broken "
|
||||
"and is required for functionality."
|
||||
"".format(generic_interface, related_interface), "WARN")
|
||||
# Normal case relation ID exists but no related unit
|
||||
# (joining)
|
||||
else:
|
||||
juju_log("{} relations's interface, {}, is related but has "
|
||||
"no units in the relation."
|
||||
"".format(generic_interface, related_interface), "INFO")
|
||||
# Related unit exists and data missing on the relation
|
||||
else:
|
||||
juju_log("{} relation's interface, {}, is related awaiting "
|
||||
"the following data from the relationship: {}. "
|
||||
"".format(generic_interface, related_interface,
|
||||
", ".join(missing_data)), "INFO")
|
||||
if state != 'blocked':
|
||||
state = 'waiting'
|
||||
if generic_interface not in incomplete_relations \
|
||||
and generic_interface not in missing_relations:
|
||||
incomplete_relations.append(generic_interface)
|
||||
|
||||
if missing_relations:
|
||||
message = "Missing relations: {}".format(", ".join(missing_relations))
|
||||
if incomplete_relations:
|
||||
message += "; incomplete relations: {}" \
|
||||
"".format(", ".join(incomplete_relations))
|
||||
state = 'blocked'
|
||||
elif incomplete_relations:
|
||||
message = "Incomplete relations: {}" \
|
||||
"".format(", ".join(incomplete_relations))
|
||||
state = 'waiting'
|
||||
|
||||
# Run charm specific checks
|
||||
if charm_func:
|
||||
charm_state, charm_message = charm_func(configs)
|
||||
if charm_state != 'active' and charm_state != 'unknown':
|
||||
state = workload_state_compare(state, charm_state)
|
||||
if message:
|
||||
message = "{} {}".format(message, charm_message)
|
||||
else:
|
||||
message = charm_message
|
||||
|
||||
# Set to active if all requirements have been met
|
||||
if state == 'active':
|
||||
message = "Unit is ready"
|
||||
juju_log(message, "INFO")
|
||||
|
||||
status_set(state, message)
|
||||
|
||||
|
||||
def workload_state_compare(current_workload_state, workload_state):
|
||||
""" Return highest priority of two states"""
|
||||
hierarchy = {'unknown': -1,
|
||||
'active': 0,
|
||||
'maintenance': 1,
|
||||
'waiting': 2,
|
||||
'blocked': 3,
|
||||
}
|
||||
|
||||
if hierarchy.get(workload_state) is None:
|
||||
workload_state = 'unknown'
|
||||
if hierarchy.get(current_workload_state) is None:
|
||||
current_workload_state = 'unknown'
|
||||
|
||||
# Set workload_state based on hierarchy of statuses
|
||||
if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
|
||||
return current_workload_state
|
||||
else:
|
||||
return workload_state
|
||||
|
||||
|
||||
def incomplete_relation_data(configs, required_interfaces):
|
||||
"""
|
||||
Check complete contexts against required_interfaces
|
||||
Return dictionary of incomplete relation data.
|
||||
|
||||
configs is an OSConfigRenderer object with configs registered
|
||||
|
||||
required_interfaces is a dictionary of required general interfaces
|
||||
with dictionary values of possible specific interfaces.
|
||||
Example:
|
||||
required_interfaces = {'database': ['shared-db', 'pgsql-db']}
|
||||
|
||||
The interface is said to be satisfied if anyone of the interfaces in the
|
||||
list has a complete context.
|
||||
|
||||
Return dictionary of incomplete or missing required contexts with relation
|
||||
status of interfaces and any missing data points. Example:
|
||||
{'message':
|
||||
{'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
|
||||
'zeromq-configuration': {'related': False}},
|
||||
'identity':
|
||||
{'identity-service': {'related': False}},
|
||||
'database':
|
||||
{'pgsql-db': {'related': False},
|
||||
'shared-db': {'related': True}}}
|
||||
"""
|
||||
complete_ctxts = configs.complete_contexts()
|
||||
incomplete_relations = []
|
||||
for svc_type in required_interfaces.keys():
|
||||
# Avoid duplicates
|
||||
found_ctxt = False
|
||||
for interface in required_interfaces[svc_type]:
|
||||
if interface in complete_ctxts:
|
||||
found_ctxt = True
|
||||
if not found_ctxt:
|
||||
incomplete_relations.append(svc_type)
|
||||
incomplete_context_data = {}
|
||||
for i in incomplete_relations:
|
||||
incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
|
||||
return incomplete_context_data
|
||||
|
||||
|
||||
def do_action_openstack_upgrade(package, upgrade_callback, configs):
|
||||
"""Perform action-managed OpenStack upgrade.
|
||||
|
||||
|
@ -182,9 +182,9 @@ def config_changed():
|
||||
if git_install_requested():
|
||||
if config_value_changed('openstack-origin-git'):
|
||||
git_install(config('openstack-origin-git'))
|
||||
else:
|
||||
elif not config('action-managed-upgrade'):
|
||||
if openstack_upgrade_available('nova-common'):
|
||||
CONFIGS = do_openstack_upgrade()
|
||||
CONFIGS = do_openstack_upgrade(CONFIGS)
|
||||
[neutron_api_relation_joined(rid=rid, remote_restart=True)
|
||||
for rid in relation_ids('neutron-api')]
|
||||
save_script_rc()
|
||||
|
@ -624,7 +624,7 @@ def _do_openstack_upgrade(new_src):
|
||||
return configs
|
||||
|
||||
|
||||
def do_openstack_upgrade():
|
||||
def do_openstack_upgrade(configs):
|
||||
new_src = config('openstack-origin')
|
||||
if new_src[:6] != 'cloud:':
|
||||
raise ValueError("Unable to perform upgrade to %s" % new_src)
|
||||
|
76
unit_tests/test_actions_openstack_upgrade.py
Normal file
76
unit_tests/test_actions_openstack_upgrade.py
Normal file
@ -0,0 +1,76 @@
|
||||
from mock import patch, MagicMock
|
||||
import os
|
||||
|
||||
os.environ['JUJU_UNIT_NAME'] = 'nova-cloud-controller'
|
||||
|
||||
|
||||
with patch('charmhelpers.core.hookenv.config') as config:
|
||||
config.return_value = 'nova'
|
||||
import nova_cc_utils as utils # noqa
|
||||
|
||||
_reg = utils.register_configs
|
||||
_map = utils.restart_map
|
||||
|
||||
utils.register_configs = MagicMock()
|
||||
utils.restart_map = MagicMock()
|
||||
|
||||
with patch('nova_cc_utils.guard_map') as gmap:
|
||||
with patch('charmhelpers.core.hookenv.config') as config:
|
||||
config.return_value = False
|
||||
gmap.return_value = {}
|
||||
import openstack_upgrade
|
||||
|
||||
utils.register_configs = _reg
|
||||
utils.restart_map = _map
|
||||
|
||||
from test_utils import (
|
||||
CharmTestCase
|
||||
)
|
||||
|
||||
TO_PATCH = [
|
||||
'do_openstack_upgrade',
|
||||
'relation_ids',
|
||||
'neutron_api_relation_joined',
|
||||
'config_changed',
|
||||
]
|
||||
|
||||
|
||||
class TestNovaCCUpgradeActions(CharmTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestNovaCCUpgradeActions, self).setUp(openstack_upgrade,
|
||||
TO_PATCH)
|
||||
|
||||
@patch('charmhelpers.contrib.openstack.utils.config')
|
||||
@patch('charmhelpers.contrib.openstack.utils.action_set')
|
||||
@patch('charmhelpers.contrib.openstack.utils.git_install_requested')
|
||||
@patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
|
||||
def test_openstack_upgrade_true(self, upgrade_avail, git_requested,
|
||||
action_set, config):
|
||||
git_requested.return_value = False
|
||||
upgrade_avail.return_value = True
|
||||
config.return_value = True
|
||||
self.relation_ids.return_value = ['relid1']
|
||||
|
||||
openstack_upgrade.openstack_upgrade()
|
||||
|
||||
self.assertTrue(self.do_openstack_upgrade.called)
|
||||
self.assertTrue(
|
||||
self.neutron_api_relation_joined.called_with(rid='relid1',
|
||||
remote_restart=True))
|
||||
self.assertTrue(self.config_changed.called)
|
||||
|
||||
@patch('charmhelpers.contrib.openstack.utils.config')
|
||||
@patch('charmhelpers.contrib.openstack.utils.action_set')
|
||||
@patch('charmhelpers.contrib.openstack.utils.git_install_requested')
|
||||
@patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
|
||||
def test_openstack_upgrade_false(self, upgrade_avail, git_requested,
|
||||
action_set, config):
|
||||
git_requested.return_value = False
|
||||
upgrade_avail.return_value = True
|
||||
config.return_value = False
|
||||
|
||||
openstack_upgrade.openstack_upgrade()
|
||||
|
||||
self.assertFalse(self.do_openstack_upgrade.called)
|
||||
self.assertFalse(self.config_changed.called)
|
@ -115,8 +115,8 @@ class NovaCCHooksTests(CharmTestCase):
|
||||
hooks.install()
|
||||
self.apt_install.assert_called_with(
|
||||
['nova-scheduler', 'nova-api-ec2'], fatal=True)
|
||||
self.execd_preinstall.assert_called()
|
||||
self.disable_services.assert_called()
|
||||
self.assertTrue(self.execd_preinstall.called)
|
||||
self.assertTrue(self.disable_services.called)
|
||||
self.cmd_all_services.assert_called_with('stop')
|
||||
|
||||
def test_install_hook_git(self):
|
||||
@ -141,8 +141,8 @@ class NovaCCHooksTests(CharmTestCase):
|
||||
hooks.install()
|
||||
self.git_install.assert_called_with(projects_yaml)
|
||||
self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)
|
||||
self.execd_preinstall.assert_called()
|
||||
self.disable_services.assert_called()
|
||||
self.assertTrue(self.execd_preinstall.called)
|
||||
self.assertTrue(self.disable_services.called)
|
||||
self.cmd_all_services.assert_called_with('stop')
|
||||
|
||||
@patch.object(hooks, 'filter_installed_packages')
|
||||
|
@ -619,7 +619,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
self.relation_ids.return_value = []
|
||||
utils.migrate_nova_database()
|
||||
check_output.assert_called_with(['nova-manage', 'db', 'sync'])
|
||||
self.enable_services.assert_called()
|
||||
self.assertTrue(self.enable_services.called)
|
||||
self.cmd_all_services.assert_called_with('start')
|
||||
|
||||
@patch('subprocess.check_output')
|
||||
@ -629,7 +629,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
utils.migrate_nova_database()
|
||||
check_output.assert_called_with(['nova-manage', 'db', 'sync'])
|
||||
self.peer_store.assert_called_with('dbsync_state', 'complete')
|
||||
self.enable_services.assert_called()
|
||||
self.assertTrue(self.enable_services.called)
|
||||
self.cmd_all_services.assert_called_with('start')
|
||||
|
||||
@patch.object(utils, 'get_step_upgrade_source')
|
||||
@ -647,7 +647,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
'icehouse']
|
||||
self.is_elected_leader.return_value = True
|
||||
self.relation_ids.return_value = []
|
||||
utils.do_openstack_upgrade()
|
||||
utils.do_openstack_upgrade(self.register_configs())
|
||||
expected = [call(['stamp', 'grizzly']), call(['upgrade', 'head']),
|
||||
call(['stamp', 'havana']), call(['upgrade', 'head'])]
|
||||
self.assertEquals(self.neutron_db_manage.call_args_list, expected)
|
||||
@ -655,7 +655,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
self.apt_upgrade.assert_called_with(options=DPKG_OPTS, fatal=True,
|
||||
dist=True)
|
||||
self.apt_install.assert_called_with(determine_packages(), fatal=True)
|
||||
expected = [call(release='havana'), call(release='icehouse')]
|
||||
expected = [call(), call(release='havana'), call(release='icehouse')]
|
||||
self.assertEquals(self.register_configs.call_args_list, expected)
|
||||
self.assertEquals(self.ml2_migration.call_count, 1)
|
||||
self.assertTrue(migrate_nova_database.call_count, 2)
|
||||
@ -673,7 +673,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
self.get_os_codename_install_source.return_value = 'icehouse'
|
||||
self.is_elected_leader.return_value = True
|
||||
self.relation_ids.return_value = []
|
||||
utils.do_openstack_upgrade()
|
||||
utils.do_openstack_upgrade(self.register_configs())
|
||||
self.neutron_db_manage.assert_called_with(['upgrade', 'head'])
|
||||
self.apt_update.assert_called_with(fatal=True)
|
||||
self.apt_upgrade.assert_called_with(options=DPKG_OPTS, fatal=True,
|
||||
@ -696,7 +696,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
self.get_os_codename_install_source.return_value = 'juno'
|
||||
self.is_elected_leader.return_value = True
|
||||
self.relation_ids.return_value = []
|
||||
utils.do_openstack_upgrade()
|
||||
utils.do_openstack_upgrade(self.register_configs())
|
||||
neutron_db_calls = [call(['stamp', 'icehouse']),
|
||||
call(['upgrade', 'head'])]
|
||||
self.neutron_db_manage.assert_has_calls(neutron_db_calls,
|
||||
@ -722,7 +722,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
self.get_os_codename_install_source.return_value = 'kilo'
|
||||
self.is_elected_leader.return_value = True
|
||||
self.relation_ids.return_value = []
|
||||
utils.do_openstack_upgrade()
|
||||
utils.do_openstack_upgrade(self.register_configs())
|
||||
self.assertEquals(self.neutron_db_manage.call_count, 0)
|
||||
self.apt_update.assert_called_with(fatal=True)
|
||||
self.apt_upgrade.assert_called_with(options=DPKG_OPTS, fatal=True,
|
||||
@ -741,7 +741,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
_file.read = MagicMock()
|
||||
_file.readline.return_value = ("deb url"
|
||||
" precise-updates/grizzly main")
|
||||
utils.do_openstack_upgrade()
|
||||
utils.do_openstack_upgrade(self.register_configs())
|
||||
expected = [call('cloud:precise-havana'),
|
||||
call('cloud:precise-icehouse')]
|
||||
self.assertEquals(_do_openstack_upgrade.call_args_list, expected)
|
||||
@ -754,7 +754,7 @@ class NovaCCUtilsTests(CharmTestCase):
|
||||
with patch_open() as (_open, _file):
|
||||
_file.read = MagicMock()
|
||||
_file.readline.return_value = "deb url precise-updates/havana main"
|
||||
utils.do_openstack_upgrade()
|
||||
utils.do_openstack_upgrade(self.register_configs())
|
||||
expected = [call('cloud:precise-icehouse')]
|
||||
self.assertEquals(_do_openstack_upgrade.call_args_list, expected)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user