Validate instance_info.traits against node traits
The ironic node traits spec calls out that traits added to instance_info.traits should be validated against the node's traits. All traits in instance_info.traits should exist in the node's traits. This protects us against race conditions between traits being removed from a node in ironic, and the node's resource provider's traits being updated in placement. This change adds validation to do_node_deploy() and validate_driver_interfaces() in the conductor manager, ensuring that all instance traits are also node traits. Change-Id: I956f8285fe428b2bdf8822e4a308f5c2a1675836 Closes-Bug: #1755146 Related-Bug: #1722194
This commit is contained in:
parent
c70bc0bd2e
commit
d1cd215c66
@ -822,6 +822,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
try:
|
try:
|
||||||
task.driver.power.validate(task)
|
task.driver.power.validate(task)
|
||||||
task.driver.deploy.validate(task)
|
task.driver.deploy.validate(task)
|
||||||
|
utils.validate_instance_info_traits(task.node)
|
||||||
except exception.InvalidParameterValue as e:
|
except exception.InvalidParameterValue as e:
|
||||||
raise exception.InstanceDeployFailure(
|
raise exception.InstanceDeployFailure(
|
||||||
_("Failed to validate deploy or power info for node "
|
_("Failed to validate deploy or power info for node "
|
||||||
@ -1869,6 +1870,8 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
if iface:
|
if iface:
|
||||||
try:
|
try:
|
||||||
iface.validate(task)
|
iface.validate(task)
|
||||||
|
if iface_name == 'deploy':
|
||||||
|
utils.validate_instance_info_traits(task.node)
|
||||||
result = True
|
result = True
|
||||||
except (exception.InvalidParameterValue,
|
except (exception.InvalidParameterValue,
|
||||||
exception.UnsupportedDriverExtension) as e:
|
exception.UnsupportedDriverExtension) as e:
|
||||||
|
@ -16,6 +16,7 @@ from oslo_config import cfg
|
|||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_service import loopingcall
|
from oslo_service import loopingcall
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
|
import six
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
@ -744,3 +745,39 @@ def remove_node_rescue_password(node, save=True):
|
|||||||
node.instance_info = instance_info
|
node.instance_info = instance_info
|
||||||
if save:
|
if save:
|
||||||
node.save()
|
node.save()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_instance_info_traits(node):
|
||||||
|
"""Validate traits in instance_info.
|
||||||
|
|
||||||
|
All traits in instance_info must also exist as node traits.
|
||||||
|
|
||||||
|
:param node: an Ironic node object.
|
||||||
|
:raises: InvalidParameterValue if the instance traits are badly formatted,
|
||||||
|
or contain traits that are not set on the node.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def invalid():
|
||||||
|
err = (_("Error parsing traits from Node %(node)s instance_info "
|
||||||
|
"field. A list of strings is expected.")
|
||||||
|
% {"node": node.uuid})
|
||||||
|
raise exception.InvalidParameterValue(err)
|
||||||
|
|
||||||
|
if not node.instance_info.get('traits'):
|
||||||
|
return
|
||||||
|
instance_traits = node.instance_info['traits']
|
||||||
|
if not isinstance(instance_traits, list):
|
||||||
|
invalid()
|
||||||
|
if not all(isinstance(t, six.string_types) for t in instance_traits):
|
||||||
|
invalid()
|
||||||
|
|
||||||
|
# TODO(mgoddard): Remove the obj_attr_is_set() call in Rocky
|
||||||
|
# when all node objects will have a traits field.
|
||||||
|
node_traits = (node.traits.get_trait_names()
|
||||||
|
if node.obj_attr_is_set('traits') else [])
|
||||||
|
missing = set(instance_traits) - set(node_traits)
|
||||||
|
if missing:
|
||||||
|
err = (_("Cannot specify instance traits that are not also set on the "
|
||||||
|
"node. Node %(node)s is missing traits %(traits)s") %
|
||||||
|
{"node": node.uuid, "traits": ", ".join(missing)})
|
||||||
|
raise exception.InvalidParameterValue(err)
|
||||||
|
@ -1225,6 +1225,11 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin,
|
|||||||
mock_iwdi):
|
mock_iwdi):
|
||||||
self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi)
|
self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi)
|
||||||
|
|
||||||
|
@mock.patch.object(conductor_utils, 'validate_instance_info_traits')
|
||||||
|
def test_do_node_deploy_traits_validate_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):
|
def test_do_node_deploy_partial_ok(self, mock_iwdi):
|
||||||
mock_iwdi.return_value = False
|
mock_iwdi.return_value = False
|
||||||
self._start_service()
|
self._start_service()
|
||||||
@ -3407,6 +3412,23 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
|
|||||||
|
|
||||||
mock_iwdi.assert_called_once_with(self.context, node.instance_info)
|
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_instance_traits(
|
||||||
|
self, mock_iwdi):
|
||||||
|
mock_iwdi.return_value = False
|
||||||
|
node = obj_utils.create_test_node(self.context, driver='fake',
|
||||||
|
network_interface='noop')
|
||||||
|
with mock.patch(
|
||||||
|
'ironic.conductor.utils.validate_instance_info_traits'
|
||||||
|
) as ii_traits:
|
||||||
|
reason = 'fake reason'
|
||||||
|
ii_traits.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',
|
@mock.patch.object(manager.ConductorManager, '_fail_if_in_state',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor')
|
@mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor')
|
||||||
|
@ -1766,3 +1766,51 @@ class MiscTestCase(db_base.DbTestCase):
|
|||||||
|
|
||||||
def test_remove_node_rescue_password_save_false(self):
|
def test_remove_node_rescue_password_save_false(self):
|
||||||
self._test_remove_node_rescue_password(save=False)
|
self._test_remove_node_rescue_password(save=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateInstanceInfoTraitsTestCase(tests_base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ValidateInstanceInfoTraitsTestCase, self).setUp()
|
||||||
|
self.node = obj_utils.get_test_node(self.context, driver='fake',
|
||||||
|
traits=['trait1', 'trait2'])
|
||||||
|
|
||||||
|
def test_validate_instance_info_traits_no_instance_traits(self):
|
||||||
|
conductor_utils.validate_instance_info_traits(self.node)
|
||||||
|
|
||||||
|
def test_validate_instance_info_traits_empty_instance_traits(self):
|
||||||
|
self.node.instance_info['traits'] = []
|
||||||
|
conductor_utils.validate_instance_info_traits(self.node)
|
||||||
|
|
||||||
|
def test_parse_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):
|
||||||
|
self.node.instance_info['traits'] = ['trait1', {}]
|
||||||
|
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||||
|
'Error parsing traits from Node',
|
||||||
|
conductor_utils.validate_instance_info_traits,
|
||||||
|
self.node)
|
||||||
|
|
||||||
|
def test_validate_instance_info_traits(self):
|
||||||
|
self.node.instance_info['traits'] = ['trait1', 'trait2']
|
||||||
|
conductor_utils.validate_instance_info_traits(self.node)
|
||||||
|
|
||||||
|
def test_validate_instance_info_traits_missing(self):
|
||||||
|
self.node.instance_info['traits'] = ['trait1', 'trait3']
|
||||||
|
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||||
|
'Cannot specify instance traits that are not',
|
||||||
|
conductor_utils.validate_instance_info_traits,
|
||||||
|
self.node)
|
||||||
|
|
||||||
|
def test_validate_instance_info_traits_no_node_traits(self):
|
||||||
|
self.node.instance_info['traits'] = ['trait1', 'trait2']
|
||||||
|
delattr(self.node, 'traits')
|
||||||
|
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||||
|
'Cannot specify instance traits that are not',
|
||||||
|
conductor_utils.validate_instance_info_traits,
|
||||||
|
self.node)
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Fixes an issue where a node's ``instance_info.traits`` field could be
|
||||||
|
incorrectly formatted, or contain traits that are not traits of the node.
|
||||||
|
When validating drivers and prior to deployment, the Bare Metal service now
|
||||||
|
validates that a node's traits include all the traits in its
|
||||||
|
``instance_info.traits`` field. See `bug 1755146
|
||||||
|
<https://bugs.launchpad.net/ironic/+bug/1755146>`_ for details.
|
Loading…
Reference in New Issue
Block a user