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
This commit is contained in:
Brent Eagles 2018-12-14 16:52:52 -03:30 committed by Kamil Sambor
parent b19daa966f
commit 379f886119
6 changed files with 217 additions and 1 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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']

View File

@ -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(

View File

@ -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))

View File

@ -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