Correct handling of capabilities on node update

When updating nodes we do not convert capabilities from the supported
dict representation to internal string representation. This may result
in incorrect capabilities after an update. This patch fixes it.

Also enforce alphabetical order in capabilities to prevent them
from reordering on updates.

Conflicts:
	tripleo_common/utils/nodes.py

Change-Id: I098409e5ff63b3c3719563de8144fda5bf482bdf
Closes-Bug: #1802318
(cherry picked from commit 2c225f1f2a)
This commit is contained in:
Dmitry Tantsur 2018-11-08 16:00:06 +01:00
parent c4f94e31a9
commit 4f0991110a
3 changed files with 68 additions and 4 deletions

View File

@ -0,0 +1,4 @@
---
fixes:
- |
Node update now works correctly when capabilities are specified as a dict.

View File

@ -511,6 +511,52 @@ class NodesTest(base.TestCase):
nodes._update_or_register_ironic_node(node, node_map, client=ironic)
ironic.node.update.assert_called_once_with(1, mock.ANY)
def test_register_update_caps_dict(self):
interfaces = {'boot_interface': 'pxe',
'console_interface': 'ipmitool-socat',
'deploy_interface': 'direct',
'inspect_interface': 'inspector',
'management_interface': 'ipmitool',
'network_interface': 'neutron',
'power_interface': 'ipmitool',
'raid_interface': 'agent',
'storage_interface': 'cinder',
'vendor_interface': 'ipmitool'}
node = self._get_node()
node.update(interfaces)
node['root_device'] = {'serial': 'abcdef'}
node['capabilities'] = {'profile': 'compute', 'num_nics': 6}
ironic = mock.MagicMock()
node_map = {'mac': {'aaa': 1}}
def side_effect(*args, **kwargs):
update_patch = [
{'path': '/name', 'value': 'node1'},
{'path': '/driver_info/ipmi_password', 'value': 'random'},
{'path': '/driver_info/ipmi_address', 'value': 'foo.bar'},
{'path': '/properties/memory_mb', 'value': '2048'},
{'path': '/properties/local_gb', 'value': '30'},
{'path': '/properties/cpu_arch', 'value': 'amd64'},
{'path': '/properties/cpus', 'value': '1'},
{'path': '/properties/capabilities',
'value': 'num_nics:6,profile:compute'},
{'path': '/properties/root_device',
'value': {'serial': 'abcdef'}},
{'path': '/driver_info/ipmi_username', 'value': 'test'}]
for iface, value in interfaces.items():
update_patch.append({'path': '/%s' % iface, 'value': value})
for key in update_patch:
key['op'] = 'add'
self.assertThat(update_patch,
matchers.MatchesSetwise(*(map(matchers.Equals,
args[1]))))
return mock.Mock(uuid='uuid1')
ironic.node.update.side_effect = side_effect
nodes._update_or_register_ironic_node(node, node_map, client=ironic)
ironic.node.update.assert_called_once_with(1, mock.ANY)
def test_register_update_with_images(self):
node = self._get_node()
node['kernel_id'] = 'image-k'

View File

@ -451,8 +451,8 @@ _NON_DRIVER_FIELDS = {'cpu': '/properties/cpus',
'name': '/name',
'resource_class': '/resource_class',
'kernel_id': '/driver_info/deploy_kernel',
'ramdisk_id': '/driver_info/deploy_ramdisk',
'capabilities': '/properties/capabilities'}
'ramdisk_id': '/driver_info/deploy_ramdisk'}
_NON_DRIVER_FIELDS.update({field: '/%s' % field
for field in _KNOWN_INTERFACE_FIELDS})
@ -471,6 +471,10 @@ def _update_or_register_ironic_node(node, node_map, client):
if field in node:
patched[path] = node.pop(field)
if 'capabilities' in node:
patched['/properties/capabilities'] = dict_to_capabilities(
node.pop('capabilities'))
driver_info = handler.convert(node)
for key, value in driver_info.items():
patched['/driver_info/%s' % key] = value
@ -536,6 +540,10 @@ def register_all_nodes(nodes_list, client, remove=False, glance_client=None,
return seen
# These fields are treated specially during enrolling/updating
_SPECIAL_NON_DRIVER_FIELDS = {'mac', 'pm_type', 'capabilities'}
def validate_nodes(nodes_list):
"""Validate all nodes list.
@ -599,7 +607,7 @@ def validate_nodes(nodes_list):
for field in node:
converted = handler.convert_key(field)
if (converted is None and field not in _NON_DRIVER_FIELDS and
field not in ('mac', 'pm_type')):
field not in _SPECIAL_NON_DRIVER_FIELDS):
failures.append((index, 'Unknown field %s' % field))
if failures:
@ -609,8 +617,14 @@ def validate_nodes(nodes_list):
def dict_to_capabilities(caps_dict):
"""Convert a dictionary into a string with the capabilities syntax."""
if isinstance(caps_dict, six.string_types):
return caps_dict
# NOTE(dtantsur): sort capabilities so that their order does not change
# between updates.
items = sorted(caps_dict.items(), key=lambda tpl: tpl[0])
return ','.join(["%s:%s" % (key, value)
for key, value in caps_dict.items()
for key, value in items
if value is not None])