diff --git a/hooks/neutron_api_hooks.py b/hooks/neutron_api_hooks.py index b058ead5..fde9e188 100755 --- a/hooks/neutron_api_hooks.py +++ b/hooks/neutron_api_hooks.py @@ -11,6 +11,7 @@ from charmhelpers.core.hookenv import ( UnregisteredHookError, config, is_relation_made, + local_unit, log, ERROR, relation_get, @@ -23,6 +24,7 @@ from charmhelpers.core.hookenv import ( from charmhelpers.core.host import ( restart_on_change, service_reload, + service_restart, ) from charmhelpers.fetch import ( @@ -37,10 +39,12 @@ from charmhelpers.contrib.openstack.utils import ( git_install_requested, openstack_upgrade_available, os_requires_version, + os_release, sync_db_with_multi_ipv6_addresses ) from neutron_api_utils import ( + CLUSTER_RES, NEUTRON_CONF, api_port, determine_packages, @@ -49,6 +53,7 @@ from neutron_api_utils import ( git_install, dvr_router_present, l3ha_router_present, + migrate_neutron_database, neutron_ready, register_configs, restart_map, @@ -66,6 +71,7 @@ from neutron_api_context import ( from charmhelpers.contrib.hahelpers.cluster import ( get_hacluster_config, + is_elected_leader, ) from charmhelpers.payload.execd import execd_preinstall @@ -91,6 +97,25 @@ hooks = Hooks() CONFIGS = register_configs() +def conditional_neutron_migration(): + if os_release('neutron-server') < 'kilo': + log('Not running neutron database migration as migrations are handled ' + 'by the neutron-server process or nova-cloud-controller charm.') + return + + if is_elected_leader(CLUSTER_RES): + allowed_units = relation_get('allowed_units') + if allowed_units and local_unit() in allowed_units.split(): + migrate_neutron_database() + service_restart('neutron-server') + else: + log('Not running neutron database migration, either no' + ' allowed_units or this unit is not present') + return + else: + log('Not running neutron database migration, not leader') + + def configure_https(): ''' Enables SSL API Apache config if appropriate and kicks identity-service @@ -231,12 +256,14 @@ def db_changed(): log('shared-db relation incomplete. Peer not ready?') return CONFIGS.write_all() + conditional_neutron_migration() @hooks.hook('pgsql-db-relation-changed') @restart_on_change(restart_map()) def postgresql_neutron_db_changed(): CONFIGS.write(NEUTRON_CONF) + conditional_neutron_migration() @hooks.hook('amqp-relation-broken', diff --git a/hooks/neutron_api_utils.py b/hooks/neutron_api_utils.py index 73af522a..433a1d72 100644 --- a/hooks/neutron_api_utils.py +++ b/hooks/neutron_api_utils.py @@ -3,6 +3,7 @@ from copy import deepcopy from functools import partial import os import shutil +import subprocess from base64 import b64encode from charmhelpers.contrib.openstack import context, templating from charmhelpers.contrib.openstack.neutron import ( @@ -258,6 +259,7 @@ def do_openstack_upgrade(configs): :param configs: The charms main OSConfigRenderer object. """ + cur_os_rel = os_release('neutron-server') new_src = config('openstack-origin') new_os_rel = get_os_codename_install_source(new_src) @@ -279,6 +281,38 @@ def do_openstack_upgrade(configs): # set CONFIGS to load templates from new release configs.set_release(openstack_release=new_os_rel) + # Before kilo it's nova-cloud-controllers job + if new_os_rel >= 'kilo': + stamp_neutron_database(cur_os_rel) + migrate_neutron_database() + + +def stamp_neutron_database(release): + '''Stamp the database with the current release before upgrade.''' + log('Stamping the neutron database with release %s.' % release) + plugin = config('neutron-plugin') + cmd = ['neutron-db-manage', + '--config-file', NEUTRON_CONF, + '--config-file', neutron_plugin_attribute(plugin, + 'config', + 'neutron'), + 'stamp', + release] + subprocess.check_output(cmd) + + +def migrate_neutron_database(): + '''Initializes a new database or upgrades an existing database.''' + log('Migrating the neutron database.') + plugin = config('neutron-plugin') + cmd = ['neutron-db-manage', + '--config-file', NEUTRON_CONF, + '--config-file', neutron_plugin_attribute(plugin, + 'config', + 'neutron'), + 'upgrade', + 'head'] + subprocess.check_output(cmd) def get_topics(): diff --git a/unit_tests/test_neutron_api_hooks.py b/unit_tests/test_neutron_api_hooks.py index 9f2e28c9..70ee610c 100644 --- a/unit_tests/test_neutron_api_hooks.py +++ b/unit_tests/test_neutron_api_hooks.py @@ -34,6 +34,7 @@ TO_PATCH = [ 'determine_ports', 'do_openstack_upgrade', 'dvr_router_present', + 'local_unit', 'l3ha_router_present', 'execd_preinstall', 'filter_installed_packages', @@ -42,15 +43,19 @@ TO_PATCH = [ 'get_l2population', 'get_overlay_network_type', 'git_install', + 'is_elected_leader', 'is_relation_made', 'log', + 'migrate_neutron_database', 'neutron_ready', 'open_port', 'openstack_upgrade_available', + 'os_release', 'os_requires_version', 'relation_get', 'relation_ids', 'relation_set', + 'service_restart', 'unit_get', 'get_iface_for_address', 'get_netmask_for_address', @@ -292,19 +297,23 @@ class NeutronAPIHooksTests(CharmTestCase): 'Attempting to associate a postgresql database when' ' there is already associated a mysql one') - def test_shared_db_changed(self): + @patch.object(hooks, 'conditional_neutron_migration') + def test_shared_db_changed(self, cond_neutron_mig): self.CONFIGS.complete_contexts.return_value = ['shared-db'] self._call_hook('shared-db-relation-changed') self.assertTrue(self.CONFIGS.write_all.called) + cond_neutron_mig.assert_called_with() def test_shared_db_changed_partial_ctxt(self): self.CONFIGS.complete_contexts.return_value = [] self._call_hook('shared-db-relation-changed') self.assertFalse(self.CONFIGS.write_all.called) - def test_pgsql_db_changed(self): + @patch.object(hooks, 'conditional_neutron_migration') + def test_pgsql_db_changed(self, cond_neutron_mig): self._call_hook('pgsql-db-relation-changed') self.assertTrue(self.CONFIGS.write.called) + cond_neutron_mig.assert_called_with() def test_amqp_broken(self): self._call_hook('amqp-relation-broken') @@ -668,3 +677,46 @@ class NeutronAPIHooksTests(CharmTestCase): call('service', 'apache2', 'reload')] self.check_call.assert_called_has_calls(calls) self.assertTrue(_id_rel_joined.called) + + def test_conditional_neutron_migration_icehouse(self): + self.os_release.return_value = 'icehouse' + hooks.conditional_neutron_migration() + self.log.assert_called_with( + 'Not running neutron database migration as migrations are handled ' + 'by the neutron-server process or nova-cloud-controller charm.' + ) + + def test_conditional_neutron_migration_ncc_rel_leader_juno(self): + self.test_relation.set({ + 'allowed_units': 'neutron-api/0 neutron-api/1 neutron-api/4', + }) + self.local_unit.return_value = 'neutron-api/1' + self.is_elected_leader.return_value = True + self.os_release.return_value = 'juno' + hooks.conditional_neutron_migration() + self.log.assert_called_with( + 'Not running neutron database migration as migrations are handled' + ' by the neutron-server process or nova-cloud-controller charm.' + ) + + def test_conditional_neutron_migration_ncc_rel_leader_kilo(self): + self.test_relation.set({ + 'allowed_units': 'neutron-api/0 neutron-api/1 neutron-api/4', + }) + self.local_unit.return_value = 'neutron-api/1' + self.is_elected_leader.return_value = True + self.os_release.return_value = 'kilo' + hooks.conditional_neutron_migration() + self.migrate_neutron_database.assert_called_with() + self.service_restart.assert_called_with('neutron-server') + + def test_conditional_neutron_migration_ncc_rel_notleader(self): + self.is_elected_leader.return_value = False + self.os_release.return_value = 'juno' + hooks.conditional_neutron_migration() + self.assertFalse(self.migrate_neutron_database.called) + self.assertFalse(self.service_restart.called) + self.log.assert_called_with( + 'Not running neutron database migration as migrations are handled ' + 'by the neutron-server process or nova-cloud-controller charm.' + ) diff --git a/unit_tests/test_neutron_api_utils.py b/unit_tests/test_neutron_api_utils.py index 10678183..d6ed4e0d 100644 --- a/unit_tests/test_neutron_api_utils.py +++ b/unit_tests/test_neutron_api_utils.py @@ -3,6 +3,7 @@ from mock import MagicMock, patch, call from collections import OrderedDict from copy import deepcopy import charmhelpers.contrib.openstack.templating as templating +import charmhelpers.contrib.openstack.utils import neutron_api_context as ncontext templating.OSConfigRenderer = MagicMock() @@ -30,6 +31,7 @@ TO_PATCH = [ 'log', 'neutron_plugin_attribute', 'os_release', + 'subprocess', ] openstack_origin_git = \ @@ -73,7 +75,6 @@ class TestNeutronAPIUtils(CharmTestCase): self.config.side_effect = self.test_config.get self.test_config.set('region', 'region101') self.neutron_plugin_attribute.side_effect = _mock_npa - self.os_release.side_effect = 'trusty' def tearDown(self): # Reset cached cache @@ -182,15 +183,19 @@ class TestNeutronAPIUtils(CharmTestCase): nutils.keystone_ca_cert_b64() self.assertTrue(self.b64encode.called) + @patch.object(nutils, 'migrate_neutron_database') + @patch.object(nutils, 'stamp_neutron_database') @patch.object(nutils, 'git_install_requested') - def test_do_openstack_upgrade(self, git_requested): + def test_do_openstack_upgrade_juno(self, git_requested, + stamp_neutron_db, migrate_neutron_db): git_requested.return_value = False self.config.side_effect = self.test_config.get self.test_config.set('openstack-origin', 'cloud:trusty-juno') - self.os_release.side_effect = 'icehouse' + self.os_release.return_value = 'icehouse' self.get_os_codename_install_source.return_value = 'juno' configs = MagicMock() nutils.do_openstack_upgrade(configs) + self.os_release.assert_called_with('neutron-server') self.log.assert_called() self.configure_installation_source.assert_called_with( 'cloud:trusty-juno' @@ -209,6 +214,46 @@ class TestNeutronAPIUtils(CharmTestCase): options=dpkg_opts, fatal=True) configs.set_release.assert_called_with(openstack_release='juno') + self.assertItemsEqual(stamp_neutron_db.call_args_list, []) + self.assertItemsEqual(migrate_neutron_db.call_args_list, []) + + @patch.object(charmhelpers.contrib.openstack.utils, + 'get_os_codename_install_source') + @patch.object(nutils, 'migrate_neutron_database') + @patch.object(nutils, 'stamp_neutron_database') + @patch.object(nutils, 'git_install_requested') + def test_do_openstack_upgrade_kilo(self, git_requested, + stamp_neutron_db, migrate_neutron_db, + gsrc): + git_requested.return_value = False + self.os_release.return_value = 'juno' + self.config.side_effect = self.test_config.get + self.test_config.set('openstack-origin', 'cloud:trusty-kilo') + gsrc.return_value = 'kilo' + self.get_os_codename_install_source.return_value = 'kilo' + configs = MagicMock() + nutils.do_openstack_upgrade(configs) + self.os_release.assert_called_with('neutron-server') + self.log.assert_called() + self.configure_installation_source.assert_called_with( + 'cloud:trusty-kilo' + ) + self.apt_update.assert_called_with(fatal=True) + dpkg_opts = [ + '--option', 'Dpkg::Options::=--force-confnew', + '--option', 'Dpkg::Options::=--force-confdef', + ] + self.apt_upgrade.assert_called_with(options=dpkg_opts, + fatal=True, + dist=True) + pkgs = nutils.determine_packages() + pkgs.sort() + self.apt_install.assert_called_with(packages=pkgs, + options=dpkg_opts, + fatal=True) + configs.set_release.assert_called_with(openstack_release='kilo') + stamp_neutron_db.assert_called_with('juno') + migrate_neutron_db.assert_called_with() @patch.object(ncontext, 'IdentityServiceContext') @patch('neutronclient.v2_0.client.Client') @@ -402,3 +447,21 @@ class TestNeutronAPIUtils(CharmTestCase): call('neutron-server'), ] self.assertEquals(service_restart.call_args_list, expected) + + def test_stamp_neutron_database(self): + nutils.stamp_neutron_database('icehouse') + cmd = ['neutron-db-manage', + '--config-file', '/etc/neutron/neutron.conf', + '--config-file', '/etc/neutron/plugins/ml2/ml2_conf.ini', + 'stamp', + 'icehouse'] + self.subprocess.check_output.assert_called_with(cmd) + + def test_migrate_neutron_database(self): + nutils.migrate_neutron_database() + cmd = ['neutron-db-manage', + '--config-file', '/etc/neutron/neutron.conf', + '--config-file', '/etc/neutron/plugins/ml2/ml2_conf.ini', + 'upgrade', + 'head'] + self.subprocess.check_output.assert_called_with(cmd)