Deploy templates: conductor

Adds the conductor-side logic required to map an instance's requested
traits to zero or more deploy templates. The steps defined in those
deploy templates are combined and added to deployment steps from the
driver interfaces, and used when provisioning the node.

The deploy steps for a node that come from deploy templates are
validated during node validation, and when deploying a node.

Change-Id: Ic4ac7926a1eaeb8b84d4f9f1af23bbe54554f250
Story: 1722275
Task: 28675
This commit is contained in:
Mark Goddard 2019-01-21 18:54:01 +00:00
parent 3f6d4c6a78
commit 17a944fe9d
4 changed files with 674 additions and 66 deletions

View File

@ -853,6 +853,7 @@ class ConductorManager(base_manager.BaseConductorManager):
task.driver.power.validate(task)
task.driver.deploy.validate(task)
utils.validate_instance_info_traits(task.node)
utils.validate_deploy_templates(task)
except exception.InvalidParameterValue as e:
raise exception.InstanceDeployFailure(
_("Failed to validate deploy or power info for node "
@ -2204,6 +2205,7 @@ class ConductorManager(base_manager.BaseConductorManager):
iface.validate(task)
if iface_name == 'deploy':
utils.validate_instance_info_traits(task.node)
utils.validate_deploy_templates(task)
result = True
except (exception.InvalidParameterValue,
exception.UnsupportedDriverExtension) as e:

View File

@ -11,6 +11,8 @@
# 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 collections
import time
from oslo_config import cfg
@ -27,6 +29,7 @@ from ironic.common import network
from ironic.common import states
from ironic.conductor import notification_utils as notify_utils
from ironic.conductor import task_manager
from ironic.objects import deploy_template
from ironic.objects import fields
LOG = log.getLogger(__name__)
@ -643,6 +646,18 @@ def _deploy_step_key(step):
DEPLOYING_INTERFACE_PRIORITY[step.get('interface')])
def _sorted_steps(steps, sort_step_key):
"""Return a sorted list of steps.
:param sort_step_key: If set, this is a method (key) used to sort the steps
from highest priority to lowest priority. For steps having the same
priority, they are sorted from highest interface priority to lowest.
:returns: A list of sorted step dictionaries.
"""
# Sort the steps from higher priority to lower priority
return sorted(steps, key=sort_step_key, reverse=True)
def _get_steps(task, interfaces, get_method, enabled=False,
sort_step_key=None):
"""Get steps for task.node.
@ -672,8 +687,7 @@ def _get_steps(task, interfaces, get_method, enabled=False,
if not enabled or x['priority'] > 0]
steps.extend(interface_steps)
if sort_step_key:
# Sort the steps from higher priority to lower priority
steps = sorted(steps, key=sort_step_key, reverse=True)
steps = _sorted_steps(steps, sort_step_key)
return steps
@ -751,6 +765,74 @@ def set_node_cleaning_steps(task):
node.save()
def _get_deployment_templates(task):
"""Get deployment templates for task.node.
Return deployment templates where the name of the deployment template
matches one of the node's instance traits (the subset of the node's traits
requested by the user via a flavor or image).
:param task: A TaskManager object
:returns: a list of DeployTemplate objects.
"""
node = task.node
if not node.instance_info.get('traits'):
return []
instance_traits = node.instance_info['traits']
return deploy_template.DeployTemplate.list_by_names(task.context,
instance_traits)
def _get_steps_from_deployment_templates(task):
"""Get deployment template steps for task.node.
Deployment template steps are those steps defined in deployment templates
where the name of the deployment template matches one of the node's
instance traits (the subset of the node's traits requested by the user via
a flavor or image). There may be many such matching templates, each with a
list of steps to execute.
:param task: A TaskManager object
:returns: A list of deploy step dictionaries
"""
templates = _get_deployment_templates(task)
steps = []
step_fields = ('interface', 'step', 'args', 'priority')
for template in templates:
steps.extend([{key: step[key] for key in step_fields}
for step in template.steps])
return steps
def _get_all_deployment_steps(task):
"""Get deployment steps for task.node.
Deployment steps from matching deployment templates are combined with those
from driver interfaces and all enabled steps returned in priority order.
:param task: A TaskManager object
:raises: InstanceDeployFailure if there was a problem getting the
deploy steps.
:returns: A list of deploy step dictionaries
"""
# Gather deploy steps from deploy templates.
user_steps = _get_steps_from_deployment_templates(task)
# Gather enabled deploy steps from drivers.
driver_steps = _get_deployment_steps(task, enabled=True, sort=False)
# Remove driver steps that have been disabled or overridden by user steps.
user_step_keys = {(s['interface'], s['step']) for s in user_steps}
steps = [s for s in driver_steps
if (s['interface'], s['step']) not in user_step_keys]
# Add enabled user steps.
enabled_user_steps = [s for s in user_steps if s['priority'] > 0]
steps.extend(enabled_user_steps)
return _sorted_steps(steps, _deploy_step_key)
def set_node_deployment_steps(task):
"""Set up the node with deployment step information for deploying.
@ -761,14 +843,212 @@ def set_node_deployment_steps(task):
"""
node = task.node
driver_internal_info = node.driver_internal_info
driver_internal_info['deploy_steps'] = _get_deployment_steps(
task, enabled=True)
driver_internal_info['deploy_steps'] = _get_all_deployment_steps(task)
node.deploy_step = {}
driver_internal_info['deploy_step_index'] = None
node.driver_internal_info = driver_internal_info
node.save()
def _validate_deploy_steps_unique(user_steps):
"""Validate that deploy steps from deploy templates are unique.
:param user_steps: a list of user steps. A user step is a dictionary
with required keys 'interface', 'step', 'args', and 'priority'::
{ 'interface': <driver_interface>,
'step': <name_of_step>,
'args': {<arg1>: <value1>, ..., <argn>: <valuen>} }
For example::
{ 'interface': deploy',
'step': 'upgrade_firmware',
'args': {'force': True} }
:return: a list of validation error strings for the steps.
"""
# Check for duplicate steps. Each interface/step combination can be
# specified at most once.
errors = []
counter = collections.Counter((step['interface'], step['step'])
for step in user_steps)
for (interface, step), count in counter.items():
if count > 1:
err = (_('duplicate deploy steps for %(interface)s.%(step)s. '
'Deploy steps from all deploy templates matching a '
'node\'s instance traits cannot have the same interface '
'and step') %
{'interface': interface, 'step': step})
errors.append(err)
return errors
def _validate_user_step(task, user_step, driver_step, step_type):
"""Validate a user-specified step.
:param task: A TaskManager object
:param user_step: a user step dictionary with required keys 'interface'
and 'step', and optional keys 'args' and 'priority'::
{ 'interface': <driver_interface>,
'step': <name_of_step>,
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
'priority': <optional_priority> }
For example::
{ 'interface': deploy',
'step': 'upgrade_firmware',
'args': {'force': True} }
:param driver_step: a driver step dictionary::
{ 'interface': <driver_interface>,
'step': <name_of_step>,
'priority': <integer>
'abortable': Optional for clean steps, absent for deploy steps.
<Boolean>.
'argsinfo': Optional. A dictionary of
{<arg_name>:<arg_info_dict>} entries.
<arg_info_dict> is a dictionary with
{ 'description': <description>,
'required': <Boolean> }
}
For example::
{ 'interface': deploy',
'step': 'upgrade_firmware',
'priority': 10,
'abortable': True,
'argsinfo': {
'force': { 'description': 'Whether to force the upgrade',
'required': False } } }
:param step_type: either 'clean' or 'deploy'.
:return: a list of validation error strings for the step.
"""
errors = []
# Check that the user-specified arguments are valid
argsinfo = driver_step.get('argsinfo') or {}
user_args = user_step.get('args') or {}
invalid = set(user_args) - set(argsinfo)
if invalid:
error = (_('%(type)s step %(step)s has these invalid arguments: '
'%(invalid)s') %
{'type': step_type, 'step': user_step,
'invalid': ', '.join(invalid)})
errors.append(error)
if step_type == 'clean' or user_step['priority'] > 0:
# Check that all required arguments were specified by the user
missing = []
for (arg_name, arg_info) in argsinfo.items():
if arg_info.get('required', False) and arg_name not in user_args:
msg = arg_name
if arg_info.get('description'):
msg += ' (%(desc)s)' % {'desc': arg_info['description']}
missing.append(msg)
if missing:
error = (_('%(type)s step %(step)s is missing these required '
'keyword arguments: %(miss)s') %
{'type': step_type, 'step': user_step,
'miss': ', '.join(missing)})
errors.append(error)
if step_type == 'clean':
# Copy fields that should not be provided by a user
user_step['abortable'] = driver_step.get('abortable', False)
user_step['priority'] = driver_step.get('priority', 0)
elif user_step['priority'] > 0:
# 'core' deploy steps can only be disabled.
is_core = (driver_step['interface'] == 'deploy' and
driver_step['step'] == 'deploy')
if is_core:
error = (_('deploy step %(step)s is a core step and '
'cannot be overridden by user steps. It may be '
'disabled by setting the priority to 0')
% {'step': user_step})
errors.append(error)
return errors
def _validate_user_steps(task, user_steps, driver_steps, step_type):
"""Validate the user-specified steps.
:param task: A TaskManager object
:param user_steps: a list of user steps. A user step is a dictionary
with required keys 'interface' and 'step', and optional keys 'args'
and 'priority'::
{ 'interface': <driver_interface>,
'step': <name_of_step>,
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
'priority': <optional_priority> }
For example::
{ 'interface': deploy',
'step': 'upgrade_firmware',
'args': {'force': True} }
:param driver_steps: a list of driver steps::
{ 'interface': <driver_interface>,
'step': <name_of_step>,
'priority': <integer>
'abortable': Optional for clean steps, absent for deploy steps.
<Boolean>.
'argsinfo': Optional. A dictionary of
{<arg_name>:<arg_info_dict>} entries.
<arg_info_dict> is a dictionary with
{ 'description': <description>,
'required': <Boolean> }
}
For example::
{ 'interface': deploy',
'step': 'upgrade_firmware',
'priority': 10,
'abortable': True,
'argsinfo': {
'force': { 'description': 'Whether to force the upgrade',
'required': False } } }
:param step_type: either 'clean' or 'deploy'.
:raises: InvalidParameterValue if validation of steps fails.
:raises: NodeCleaningFailure or InstanceDeployFailure if
there was a problem getting the steps from the driver.
:return: validated steps updated with information from the driver
"""
def step_id(step):
return '.'.join([step['step'], step['interface']])
errors = []
# Convert driver steps to a dict.
driver_steps = {step_id(s): s for s in driver_steps}
for user_step in user_steps:
# Check if this user_specified step isn't supported by the driver
try:
driver_step = driver_steps[step_id(user_step)]
except KeyError:
error = (_('node does not support this %(type)s step: %(step)s')
% {'type': step_type, 'step': user_step})
errors.append(error)
continue
step_errors = _validate_user_step(task, user_step, driver_step,
step_type)
errors.extend(step_errors)
if step_type == 'deploy':
# Deploy steps should be unique across all combined templates.
dup_errors = _validate_deploy_steps_unique(user_steps)
errors.extend(dup_errors)
if errors:
raise exception.InvalidParameterValue('; '.join(errors))
return user_steps[:]
def _validate_user_clean_steps(task, user_steps):
"""Validate the user-specified clean steps.
@ -782,7 +1062,7 @@ def _validate_user_clean_steps(task, user_steps):
For example::
{ 'interface': deploy',
{ 'interface': 'deploy',
'step': 'upgrade_firmware',
'args': {'force': True} }
:raises: InvalidParameterValue if validation of clean steps fails.
@ -790,69 +1070,35 @@ def _validate_user_clean_steps(task, user_steps):
clean steps from the driver.
:return: validated clean steps update with information from the driver
"""
driver_steps = _get_cleaning_steps(task, enabled=False, sort=False)
return _validate_user_steps(task, user_steps, driver_steps, 'clean')
def step_id(step):
return '.'.join([step['step'], step['interface']])
errors = []
def _validate_user_deploy_steps(task, user_steps):
"""Validate the user-specified deploy steps.
# The clean steps from the driver. A clean step dictionary is of the form:
# { 'interface': <driver_interface>,
# 'step': <name_of_clean_step>,
# 'priority': <integer>
# 'abortable': Optional. <Boolean>.
# 'argsinfo': Optional. A dictionary of {<arg_name>:<arg_info_dict>}
# entries. <arg_info_dict> is a dictionary with
# { 'description': <description>,
# 'required': <Boolean> }
# }
driver_steps = {}
for s in _get_cleaning_steps(task, enabled=False, sort=False):
driver_steps[step_id(s)] = s
:param task: A TaskManager object
:param user_steps: a list of deploy steps. A deploy step is a dictionary
with required keys 'interface', 'step', 'args', and 'priority'::
result = []
for user_step in user_steps:
# Check if this user_specified clean step isn't supported by the driver
try:
driver_step = driver_steps[step_id(user_step)]
except KeyError:
error = (_('node does not support this clean step: %(step)s')
% {'step': user_step})
errors.append(error)
continue
{ 'interface': <driver_interface>,
'step': <name_of_deploy_step>,
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
'priority': <priority_of_deploy_step> }
# Check that the user-specified arguments are valid
argsinfo = driver_step.get('argsinfo') or {}
user_args = user_step.get('args') or {}
invalid = set(user_args) - set(argsinfo)
if invalid:
error = _('clean step %(step)s has these invalid arguments: '
'%(invalid)s') % {'step': user_step,
'invalid': ', '.join(invalid)}
errors.append(error)
For example::
# Check that all required arguments were specified by the user
missing = []
for (arg_name, arg_info) in argsinfo.items():
if arg_info.get('required', False) and arg_name not in user_args:
msg = arg_name
if arg_info.get('description'):
msg += ' (%(desc)s)' % {'desc': arg_info['description']}
missing.append(msg)
if missing:
error = _('clean step %(step)s is missing these required keyword '
'arguments: %(miss)s') % {'step': user_step,
'miss': ', '.join(missing)}
errors.append(error)
# Copy fields that should not be provided by a user
user_step['abortable'] = driver_step.get('abortable', False)
user_step['priority'] = driver_step.get('priority', 0)
result.append(user_step)
if errors:
raise exception.InvalidParameterValue('; '.join(errors))
return result
{ 'interface': 'bios',
'step': 'apply_configuration',
'args': { 'settings': [ { 'foo': 'bar' } ] },
'priority': 150 }
:raises: InvalidParameterValue if validation of deploy steps fails.
:raises: InstanceDeployFailure if there was a problem getting the deploy
steps from the driver.
:return: validated deploy steps update with information from the driver
"""
driver_steps = _get_deployment_steps(task, enabled=False, sort=False)
return _validate_user_steps(task, user_steps, driver_steps, 'deploy')
@task_manager.require_exclusive_lock
@ -1056,3 +1302,17 @@ def restore_power_state_if_needed(task, power_state_to_restore):
# enough time to apply network changes.
time.sleep(CONF.agent.neutron_agent_poll_interval * 2)
node_power_action(task, power_state_to_restore)
def validate_deploy_templates(task):
"""Validate the deploy templates for a node.
:param task: A TaskManager object
:raises: InvalidParameterValue if the instance has traits that map to
deploy steps that are unsupported by the node's driver interfaces.
:raises: InstanceDeployFailure if there was a problem getting the deploy
steps from the driver.
"""
# Gather deploy steps from matching deploy templates, validate them.
user_steps = _get_steps_from_deployment_templates(task)
_validate_user_deploy_steps(task, user_steps)

View File

@ -1325,6 +1325,11 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin,
mock_iwdi):
self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi)
@mock.patch.object(conductor_utils, 'validate_deploy_templates')
def test_do_node_deploy_validate_template_fail(self, mock_validate,
mock_iwdi):
self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi)
def test_do_node_deploy_partial_ok(self, mock_iwdi):
mock_iwdi.return_value = False
self._start_service()
@ -4795,6 +4800,23 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
self.assertEqual(reason, ret['deploy']['reason'])
mock_iwdi.assert_called_once_with(self.context, node.instance_info)
@mock.patch.object(images, 'is_whole_disk_image')
def test_validate_driver_interfaces_validation_fail_deploy_templates(
self, mock_iwdi):
mock_iwdi.return_value = False
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
network_interface='noop')
with mock.patch(
'ironic.conductor.utils.validate_deploy_templates'
) as mock_validate:
reason = 'fake reason'
mock_validate.side_effect = exception.InvalidParameterValue(reason)
ret = self.service.validate_driver_interfaces(self.context,
node.uuid)
self.assertFalse(ret['deploy']['result'])
self.assertEqual(reason, ret['deploy']['reason'])
mock_iwdi.assert_called_once_with(self.context, node.instance_info)
@mock.patch.object(manager.ConductorManager, '_fail_if_in_state',
autospec=True)
@mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor')

View File

@ -980,9 +980,16 @@ class NodeDeployStepsTestCase(db_base.DbTestCase):
'step': 'deploy_end', 'priority': 20, 'interface': 'deploy'}
self.power_disable = {
'step': 'power_disable', 'priority': 0, 'interface': 'power'}
self.deploy_core = {
'step': 'deploy', 'priority': 100, 'interface': 'deploy'}
# enabled steps
self.deploy_steps = [self.deploy_start, self.power_one,
self.deploy_middle, self.deploy_end]
# Deploy step with argsinfo.
self.deploy_raid = {
'step': 'build_raid', 'priority': 0, 'interface': 'deploy',
'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
'arg2': {'description': 'desc2'}}}
self.node = obj_utils.create_test_node(
self.context, driver='fake-hardware')
@ -1058,7 +1065,148 @@ class NodeDeployStepsTestCase(db_base.DbTestCase):
mock_power_steps.assert_called_once_with(mock.ANY, task)
mock_deploy_steps.assert_called_once_with(mock.ANY, task)
@mock.patch.object(conductor_utils, '_get_deployment_steps',
@mock.patch.object(objects.DeployTemplate, 'list_by_names')
def test__get_deployment_templates_no_traits(self, mock_list):
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
templates = conductor_utils._get_deployment_templates(task)
self.assertEqual([], templates)
self.assertFalse(mock_list.called)
@mock.patch.object(objects.DeployTemplate, 'list_by_names')
def test__get_deployment_templates(self, mock_list):
traits = ['CUSTOM_DT1', 'CUSTOM_DT2']
node = obj_utils.create_test_node(
self.context, uuid=uuidutils.generate_uuid(),
driver='fake-hardware', instance_info={'traits': traits})
template1 = obj_utils.get_test_deploy_template(self.context)
template2 = obj_utils.get_test_deploy_template(
self.context, name='CUSTOM_DT2', uuid=uuidutils.generate_uuid(),
steps=[{'interface': 'bios', 'step': 'apply_configuration',
'args': {}, 'priority': 1}])
mock_list.return_value = [template1, template2]
expected = [template1, template2]
with task_manager.acquire(
self.context, node.uuid, shared=False) as task:
templates = conductor_utils._get_deployment_templates(task)
self.assertEqual(expected, templates)
mock_list.assert_called_once_with(task.context, traits)
@mock.patch.object(conductor_utils, '_get_deployment_templates',
autospec=True)
def test__get_steps_from_deployment_templates(self, mock_templates):
template1 = obj_utils.get_test_deploy_template(self.context)
template2 = obj_utils.get_test_deploy_template(
self.context, name='CUSTOM_DT2', uuid=uuidutils.generate_uuid(),
steps=[{'interface': 'bios', 'step': 'apply_configuration',
'args': {}, 'priority': 1}])
mock_templates.return_value = [template1, template2]
step1 = template1.steps[0]
step2 = template2.steps[0]
expected = [
{
'interface': step1['interface'],
'step': step1['step'],
'args': step1['args'],
'priority': step1['priority'],
},
{
'interface': step2['interface'],
'step': step2['step'],
'args': step2['args'],
'priority': step2['priority'],
}
]
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
steps = conductor_utils._get_steps_from_deployment_templates(task)
self.assertEqual(expected, steps)
mock_templates.assert_called_once_with(task)
@mock.patch.object(conductor_utils, '_get_steps_from_deployment_templates',
autospec=True)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def _test__get_all_deployment_steps(self, user_steps, driver_steps,
expected_steps, mock_gds, mock_gsfdt):
mock_gsfdt.return_value = user_steps
mock_gds.return_value = driver_steps
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
steps = conductor_utils._get_all_deployment_steps(task)
self.assertEqual(expected_steps, steps)
mock_gsfdt.assert_called_once_with(task)
mock_gds.assert_called_once_with(task, enabled=True, sort=False)
def test__get_all_deployment_steps_no_steps(self):
# Nothing in -> nothing out.
user_steps = []
driver_steps = []
expected_steps = []
self._test__get_all_deployment_steps(user_steps, driver_steps,
expected_steps)
def test__get_all_deployment_steps_no_user_steps(self):
# Only driver steps in -> only driver steps out.
user_steps = []
driver_steps = self.deploy_steps
expected_steps = self.deploy_steps
self._test__get_all_deployment_steps(user_steps, driver_steps,
expected_steps)
def test__get_all_deployment_steps_no_driver_steps(self):
# Only user steps in -> only user steps out.
user_steps = self.deploy_steps
driver_steps = []
expected_steps = self.deploy_steps
self._test__get_all_deployment_steps(user_steps, driver_steps,
expected_steps)
def test__get_all_deployment_steps_user_and_driver_steps(self):
# Driver and user steps in -> driver and user steps out.
user_steps = self.deploy_steps[:2]
driver_steps = self.deploy_steps[2:]
expected_steps = self.deploy_steps
self._test__get_all_deployment_steps(user_steps, driver_steps,
expected_steps)
def test__get_all_deployment_steps_disable_core_steps(self):
# User steps can disable core driver steps.
user_steps = [self.deploy_core.copy()]
user_steps[0].update({'priority': 0})
driver_steps = [self.deploy_core]
expected_steps = []
self._test__get_all_deployment_steps(user_steps, driver_steps,
expected_steps)
def test__get_all_deployment_steps_override_driver_steps(self):
# User steps override non-core driver steps.
user_steps = [step.copy() for step in self.deploy_steps[:2]]
user_steps[0].update({'priority': 200})
user_steps[1].update({'priority': 100})
driver_steps = self.deploy_steps
expected_steps = user_steps + self.deploy_steps[2:]
self._test__get_all_deployment_steps(user_steps, driver_steps,
expected_steps)
def test__get_all_deployment_steps_duplicate_user_steps(self):
# Duplicate user steps override non-core driver steps.
# NOTE(mgoddard): This case is currently prevented by the API and
# conductor - the interface/step must be unique across all enabled
# steps. This test ensures that we can support this case, in case we
# choose to allow it in future.
user_steps = [self.deploy_start.copy(), self.deploy_start.copy()]
user_steps[0].update({'priority': 200})
user_steps[1].update({'priority': 100})
driver_steps = self.deploy_steps
# Each user invocation of the deploy_start step should be included, but
# not the default deploy_start from the driver.
expected_steps = user_steps + self.deploy_steps[1:]
self._test__get_all_deployment_steps(user_steps, driver_steps,
expected_steps)
@mock.patch.object(conductor_utils, '_get_all_deployment_steps',
autospec=True)
def test_set_node_deployment_steps(self, mock_steps):
mock_steps.return_value = self.deploy_steps
@ -1072,7 +1220,147 @@ class NodeDeployStepsTestCase(db_base.DbTestCase):
self.assertEqual({}, self.node.deploy_step)
self.assertIsNone(
self.node.driver_internal_info['deploy_step_index'])
mock_steps.assert_called_once_with(task, enabled=True)
mock_steps.assert_called_once_with(task)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps(self, mock_steps):
mock_steps.return_value = self.deploy_steps
user_steps = [{'step': 'deploy_start', 'interface': 'deploy',
'priority': 100},
{'step': 'power_one', 'interface': 'power',
'priority': 200}]
with task_manager.acquire(self.context, self.node.uuid) as task:
result = conductor_utils._validate_user_deploy_steps(task,
user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
self.assertEqual(user_steps, result)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_no_steps(self, mock_steps):
mock_steps.return_value = self.deploy_steps
with task_manager.acquire(self.context, self.node.uuid) as task:
conductor_utils._validate_user_deploy_steps(task, [])
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_get_steps_exception(self, mock_steps):
mock_steps.side_effect = exception.InstanceDeployFailure('bad')
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InstanceDeployFailure,
conductor_utils._validate_user_deploy_steps,
task, [])
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_not_supported(self, mock_steps):
mock_steps.return_value = self.deploy_steps
user_steps = [{'step': 'power_one', 'interface': 'power',
'priority': 200},
{'step': 'bad_step', 'interface': 'deploy',
'priority': 100}]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaisesRegex(exception.InvalidParameterValue,
"does not support.*bad_step",
conductor_utils._validate_user_deploy_steps,
task, user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_invalid_arg(self, mock_steps):
mock_steps.return_value = self.deploy_steps
user_steps = [{'step': 'power_one', 'interface': 'power',
'args': {'arg1': 'val1', 'arg2': 'val2'},
'priority': 200}]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaisesRegex(exception.InvalidParameterValue,
"power_one.*invalid.*arg1",
conductor_utils._validate_user_deploy_steps,
task, user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_missing_required_arg(self,
mock_steps):
mock_steps.return_value = [self.power_one, self.deploy_raid]
user_steps = [{'step': 'power_one', 'interface': 'power',
'priority': 200},
{'step': 'build_raid', 'interface': 'deploy',
'priority': 100}]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaisesRegex(exception.InvalidParameterValue,
"build_raid.*missing.*arg1",
conductor_utils._validate_user_deploy_steps,
task, user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_disable_non_core(self, mock_steps):
# Required arguments don't apply to disabled steps.
mock_steps.return_value = [self.power_one, self.deploy_raid]
user_steps = [{'step': 'power_one', 'interface': 'power',
'priority': 200},
{'step': 'build_raid', 'interface': 'deploy',
'priority': 0}]
with task_manager.acquire(self.context, self.node.uuid) as task:
result = conductor_utils._validate_user_deploy_steps(task,
user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
self.assertEqual(user_steps, result)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_disable_core(self, mock_steps):
mock_steps.return_value = [self.power_one, self.deploy_core]
user_steps = [{'step': 'power_one', 'interface': 'power',
'priority': 200},
{'step': 'deploy', 'interface': 'deploy', 'priority': 0}]
with task_manager.acquire(self.context, self.node.uuid) as task:
result = conductor_utils._validate_user_deploy_steps(task,
user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
self.assertEqual(user_steps, result)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_override_core(self, mock_steps):
mock_steps.return_value = [self.power_one, self.deploy_core]
user_steps = [{'step': 'power_one', 'interface': 'power',
'priority': 200},
{'step': 'deploy', 'interface': 'deploy',
'priority': 200}]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaisesRegex(exception.InvalidParameterValue,
"deploy.*is a core step",
conductor_utils._validate_user_deploy_steps,
task, user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
def test__validate_user_deploy_steps_duplicates(self, mock_steps):
mock_steps.return_value = [self.power_one, self.deploy_core]
user_steps = [{'step': 'power_one', 'interface': 'power',
'priority': 200},
{'step': 'power_one', 'interface': 'power',
'priority': 100}]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaisesRegex(exception.InvalidParameterValue,
"duplicate deploy steps for "
"power.power_one",
conductor_utils._validate_user_deploy_steps,
task, user_steps)
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
class NodeCleaningStepsTestCase(db_base.DbTestCase):
@ -2141,14 +2429,14 @@ class ValidateInstanceInfoTraitsTestCase(tests_base.TestCase):
self.node.instance_info['traits'] = []
conductor_utils.validate_instance_info_traits(self.node)
def test_parse_instance_info_traits_invalid_type(self):
def test_validate_instance_info_traits_invalid_type(self):
self.node.instance_info['traits'] = 'not-a-list'
self.assertRaisesRegex(exception.InvalidParameterValue,
'Error parsing traits from Node',
conductor_utils.validate_instance_info_traits,
self.node)
def test_parse_instance_info_traits_invalid_trait_type(self):
def test_validate_instance_info_traits_invalid_trait_type(self):
self.node.instance_info['traits'] = ['trait1', {}]
self.assertRaisesRegex(exception.InvalidParameterValue,
'Error parsing traits from Node',
@ -2165,3 +2453,39 @@ class ValidateInstanceInfoTraitsTestCase(tests_base.TestCase):
'Cannot specify instance traits that are not',
conductor_utils.validate_instance_info_traits,
self.node)
@mock.patch.object(conductor_utils, '_get_steps_from_deployment_templates',
autospec=True)
@mock.patch.object(conductor_utils, '_validate_user_deploy_steps',
autospec=True)
class ValidateDeployTemplatesTestCase(db_base.DbTestCase):
def setUp(self):
super(ValidateDeployTemplatesTestCase, self).setUp()
self.node = obj_utils.create_test_node(self.context,
driver='fake-hardware')
def test_validate_deploy_templates(self, mock_validate, mock_get):
steps = [db_utils.get_test_deploy_template_step()]
mock_get.return_value = steps
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
conductor_utils.validate_deploy_templates(task)
mock_validate.assert_called_once_with(task, steps)
def test_validate_deploy_templates_invalid_parameter_value(
self, mock_validate, mock_get):
mock_validate.side_effect = exception.InvalidParameterValue('fake')
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
self.assertRaises(exception.InvalidParameterValue,
conductor_utils.validate_deploy_templates, task)
def test_validate_deploy_templates_instance_deploy_failure(
self, mock_validate, mock_get):
mock_validate.side_effect = exception.InstanceDeployFailure('foo')
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
self.assertRaises(exception.InstanceDeployFailure,
conductor_utils.validate_deploy_templates, task)