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:
parent
3f6d4c6a78
commit
17a944fe9d
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user