diff --git a/actions.yaml b/actions.yaml index e00e5f38..ff365819 100644 --- a/actions.yaml +++ b/actions.yaml @@ -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. diff --git a/actions/openstack-upgrade b/actions/openstack-upgrade new file mode 120000 index 00000000..61793013 --- /dev/null +++ b/actions/openstack-upgrade @@ -0,0 +1 @@ +openstack_upgrade.py \ No newline at end of file diff --git a/actions/openstack_upgrade.py b/actions/openstack_upgrade.py new file mode 100755 index 00000000..d4af66fe --- /dev/null +++ b/actions/openstack_upgrade.py @@ -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() diff --git a/config.yaml b/config.yaml index b8b38a88..931cff12 100644 --- a/config.yaml +++ b/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. diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 32c28872..2f5280e6 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -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. diff --git a/hooks/nova_cc_hooks.py b/hooks/nova_cc_hooks.py index 73d77c3e..8ebc9881 100755 --- a/hooks/nova_cc_hooks.py +++ b/hooks/nova_cc_hooks.py @@ -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() diff --git a/hooks/nova_cc_utils.py b/hooks/nova_cc_utils.py index 121e2dc1..cdf18ae8 100644 --- a/hooks/nova_cc_utils.py +++ b/hooks/nova_cc_utils.py @@ -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) diff --git a/unit_tests/test_actions_openstack_upgrade.py b/unit_tests/test_actions_openstack_upgrade.py new file mode 100644 index 00000000..ed630082 --- /dev/null +++ b/unit_tests/test_actions_openstack_upgrade.py @@ -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) diff --git a/unit_tests/test_nova_cc_hooks.py b/unit_tests/test_nova_cc_hooks.py index e616efb9..66e4287d 100644 --- a/unit_tests/test_nova_cc_hooks.py +++ b/unit_tests/test_nova_cc_hooks.py @@ -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') diff --git a/unit_tests/test_nova_cc_utils.py b/unit_tests/test_nova_cc_utils.py index a0d43b00..0f156d3b 100644 --- a/unit_tests/test_nova_cc_utils.py +++ b/unit_tests/test_nova_cc_utils.py @@ -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)