From 379f886119727fe440e384234db53f48730e27ad Mon Sep 17 00:00:00 2001 From: Brent Eagles Date: Fri, 14 Dec 2018 16:52:52 -0330 Subject: [PATCH] Prevent upgrading to incompatible mechanism driver Prevent upgrading a stack to a version of tripleo templates or environment that specifies neutron mechanism drivers that are incompatible with the existing stack. Change-Id: I33fafe07326dcff4e4abb856a219d57a4c9699a1 --- .../check_ovs_upgrade-99cecd6b7bfdcf83.yaml | 8 ++ tripleo_common/actions/deployment.py | 15 +++ tripleo_common/constants.py | 2 + .../tests/actions/test_deployment.py | 5 +- tripleo_common/tests/test_update.py | 123 ++++++++++++++++++ tripleo_common/update.py | 65 +++++++++ 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/check_ovs_upgrade-99cecd6b7bfdcf83.yaml create mode 100644 tripleo_common/tests/test_update.py diff --git a/releasenotes/notes/check_ovs_upgrade-99cecd6b7bfdcf83.yaml b/releasenotes/notes/check_ovs_upgrade-99cecd6b7bfdcf83.yaml new file mode 100644 index 000000000..0e2366b5b --- /dev/null +++ b/releasenotes/notes/check_ovs_upgrade-99cecd6b7bfdcf83.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Prevent upgrading a stack to a version of tripleo templates or + environment that specifies neutron mechanism drivers that are + incompatible with the existing stack. Upgrade can be forced + by ForceNeutronDriverUpdate parameter which need to be set in + deployment parameters. \ No newline at end of file diff --git a/tripleo_common/actions/deployment.py b/tripleo_common/actions/deployment.py index 09a98a8a3..81dce9552 100644 --- a/tripleo_common/actions/deployment.py +++ b/tripleo_common/actions/deployment.py @@ -26,6 +26,7 @@ from swiftclient import exceptions as swiftexceptions from tripleo_common.actions import base from tripleo_common.actions import templates from tripleo_common import constants +from tripleo_common import update from tripleo_common.utils import overcloudrc from tripleo_common.utils import plan as plan_utils @@ -176,6 +177,20 @@ class DeployStackAction(templates.ProcessTemplatesAction): LOG.exception(err_msg) return actions.Result(error=err_msg) + if not stack_is_new: + try: + LOG.debug('Checking for compatible neutron mechanism drivers') + msg = update.check_neutron_mechanism_drivers(env, stack, + swift, + self.container) + if msg: + return actions.Result(error=msg) + except swiftexceptions.ClientException as err: + err_msg = ("Error getting template %s: %s" % ( + self.container, err)) + LOG.exception(err_msg) + return actions.Result(error=err_msg) + # process all plan files and create or update a stack processed_data = super(DeployStackAction, self).run(context) diff --git a/tripleo_common/constants.py b/tripleo_common/constants.py index 1bf7af6cf..30092eccd 100644 --- a/tripleo_common/constants.py +++ b/tripleo_common/constants.py @@ -199,3 +199,5 @@ ANSIBLE_ERRORS_FILE = 'ansible-errors.json' DEPLOYMENT_STATUS_FILE = 'deployment_status.yaml' MISTRAL_WORK_DIR = '/var/lib/mistral' + +EXCLUSIVE_NEUTRON_DRIVERS = ['ovn', 'openvswitch'] diff --git a/tripleo_common/tests/actions/test_deployment.py b/tripleo_common/tests/actions/test_deployment.py index 35fc39c93..e58ee1e31 100644 --- a/tripleo_common/tests/actions/test_deployment.py +++ b/tripleo_common/tests/actions/test_deployment.py @@ -407,6 +407,7 @@ class DeployStackActionTest(base.TestCase): error="Error during stack creation: ERROR: Oops\n") self.assertEqual(expected, action.run(mock_ctx)) + @mock.patch('tripleo_common.update.check_neutron_mechanism_drivers') @mock.patch('tripleo_common.actions.deployment.time') @mock.patch('heatclient.common.template_utils.' 'process_multiple_environments_and_files') @@ -417,7 +418,8 @@ class DeployStackActionTest(base.TestCase): def test_run_update_failed( self, get_orchestration_client_mock, mock_get_object_client, mock_get_template_contents, - mock_process_multiple_environments_and_files, mock_time): + mock_process_multiple_environments_and_files, mock_time, + mock_check_neutron_drivers): mock_ctx = mock.MagicMock() # setup swift @@ -448,6 +450,7 @@ class DeployStackActionTest(base.TestCase): # freeze time at datetime.datetime(2016, 9, 8, 16, 24, 24) mock_time.time.return_value = 1473366264 + mock_check_neutron_drivers.return_value = None action = deployment.DeployStackAction(1, 'overcloud') expected = actions.Result( diff --git a/tripleo_common/tests/test_update.py b/tripleo_common/tests/test_update.py new file mode 100644 index 000000000..c4b90d639 --- /dev/null +++ b/tripleo_common/tests/test_update.py @@ -0,0 +1,123 @@ +# Copyright 2018 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import mock + +from tripleo_common.tests import base +from tripleo_common import update + + +class TestUpdate(base.TestCase): + + def setUp(self): + super(TestUpdate, self).setUp() + + def test_successful_search_stack(self): + test_stack = [{'one': {'one_1': 'nope'}}, + {'two': [{'two_1': {'two_1_2': 'nope'}}, + {'two_2': [{'two_2_1': 'nope'}, + {'two_2_2': 'nope'}]}]}, + {'three': [{'three_1': {'three_1_2': 'nope'}}, + {'three_2': [{'three_2_1': 'nope'}, + {'three_2_2': { + 'target': ['val1', 'val2', + 'val3']}}]}]}] + result = update.search_stack(test_stack, 'target') + self.assertEqual(['val1', 'val2', 'val3'], result) + + def test_failed_search_stack(self): + test_stack = [{'one': {'one_1': 'nope'}}, + {'two': [{'two_1': {'two_1_2': 'nope'}}, + {'two_2': [{'two_2_1': 'nope'}, + {'two_2_2': 'nope'}]}]}, + {'three': [{'three_1': {'three_1_2': 'nope'}}, + {'three_2': [{'three_2_1': 'nope'}, + {'three_2_2': { + 'target': ['val1', 'val2', + 'val3']}}]}]}] + result = update.search_stack(test_stack, 'missing-target') + self.assertIsNone(result) + + def test_exclusive_neutron_drivers_not_found(self): + self.assertIsNone( + update.get_exclusive_neutron_driver(None)) + self.assertIsNone( + update.get_exclusive_neutron_driver('sriovnicswitch')) + self.assertIsNone( + update.get_exclusive_neutron_driver(['sriovnicswitch'])) + self.assertIsNone( + update.get_exclusive_neutron_driver(['sriovnicswitch', + 'odl'])) + + def test_exclusive_neutron_drivers_found(self): + for ex in ['ovn', ['ovn'], ['odl', 'ovn'], ['sriovnicswitch', 'ovn']]: + self.assertEqual('ovn', + update.get_exclusive_neutron_driver(ex)) + for ex in ['openvswitch', ['openvswitch'], + ['sriovnicswitch', 'openvswitch']]: + self.assertEqual('openvswitch', + update.get_exclusive_neutron_driver(ex)) + + @mock.patch('tripleo_common.update.search_stack', + autospec=True) + def test_update_check_mechanism_drivers_force_update(self, + mock_search_stack): + env = {'parameter_defaults': {'ForceNeutronDriverUpdate': True}} + stack = mock.Mock() + update.check_neutron_mechanism_drivers(env, stack, None, None) + self.assertFalse(mock_search_stack.called) + + @mock.patch('tripleo_common.update.get_exclusive_neutron_driver', + return_value='ovn') + @mock.patch('tripleo_common.update.search_stack', + autospec=True) + def test_update_check_mechanism_drivers_match_stack_env(self, + mock_search_stack, + mock_ex_driver): + env = {'parameter_defaults': { + 'ForceNeutronDriverUpdate': False, + 'NeutronMechanismDrivers': 'ovn' + }} + stack = mock.Mock() + self.assertIsNone(update.check_neutron_mechanism_drivers( + env, stack, None, None)) + + @mock.patch('tripleo_common.update.search_stack', + return_value='openvswitch') + def test_update_check_mechanism_drivers_mismatch_stack_env( + self, mock_search_stack): + env = {'parameter_defaults': { + 'ForceNeutronDriverUpdate': False + }} + stack = mock.Mock() + plan_client = mock.Mock() + plan_client.get_object.return_value = ( + 0, 'parameters:\n NeutronMechanismDrivers: {default: ovn}\n') + self.assertIsNotNone(update.check_neutron_mechanism_drivers( + env, stack, plan_client, None)) + + @mock.patch('tripleo_common.update.search_stack', + return_value='ovn') + def test_update_check_mechanism_drivers_match_stack_template( + self, mock_search_stack): + env = {'parameter_defaults': { + 'ForceNeutronDriverUpdate': False + }} + stack = mock.Mock() + plan_client = mock.Mock() + plan_client.get_object.return_value = ( + 0, 'parameters:\n NeutronMechanismDrivers: {default: ovn}\n') + self.assertIsNone(update.check_neutron_mechanism_drivers( + env, stack, plan_client, None)) diff --git a/tripleo_common/update.py b/tripleo_common/update.py index cf3c8cb14..d182d799e 100644 --- a/tripleo_common/update.py +++ b/tripleo_common/update.py @@ -13,6 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +from six import iteritems +import yaml + from heatclient.common import template_utils from tripleo_common import constants @@ -25,3 +28,65 @@ def add_breakpoints_cleanup_into_env(env): constants.UPDATE_RESOURCE_NAME: {'hooks': []}}}} } }) + + +def search_stack(stack_data, key_name): + if isinstance(stack_data, list): + for item in stack_data: + result = search_stack(item, key_name) + if result: + return result + elif isinstance(stack_data, dict): + for k, v in iteritems(stack_data): + if k == key_name: + return v + else: + result = search_stack(v, key_name) + if result: + return result + + +def get_exclusive_neutron_driver(drivers): + if not drivers: + return + mutually_exclusive_drivers = constants.EXCLUSIVE_NEUTRON_DRIVERS + if isinstance(drivers, str): + drivers = [drivers] + for d in mutually_exclusive_drivers: + if d in drivers: + return d + + +def check_neutron_mechanism_drivers(env, stack, plan_client, container): + force_update = env.get('parameter_defaults').get( + 'ForceNeutronDriverUpdate', False) + # Forcing an update and skip checks is need to support migrating from one + # driver to another + if force_update: + return + + driver_key = 'NeutronMechanismDrivers' + current_drivers = search_stack(stack._info, driver_key) + # TODO(beagles): We may need to move or copy this check earlier + # to automagically pull in an openvswitch ML2 compatibility driver. + current_driver = get_exclusive_neutron_driver(current_drivers) + configured_drivers = env.get('parameter_defaults').get(driver_key) + new_driver = None + if configured_drivers: + new_driver = get_exclusive_neutron_driver(configured_drivers) + else: + # TODO(beagles): we need to look for a better way to + # get the current template default value. This is fragile + # with respect to changing filenames, etc. + ml2_tmpl = plan_client.get_object( + container, 'puppet/services/neutron-plugin-ml2.yaml') + ml2_def = yaml.safe_load(ml2_tmpl[1]) + default_drivers = ml2_def.get('parameters', {}).get(driver_key, + {}).get('default') + new_driver = get_exclusive_neutron_driver(default_drivers) + + if current_driver and new_driver and current_driver != new_driver: + msg = ("Unable to switch from {} to {} neutron " + "mechanism drivers on upgrade. Please consult the " + "documentation.").format(current_driver, new_driver) + return msg