Relax extra_hardware data validation by default
We've seen reports that extra data sometimes contains empty lists. There is no point to discard everything in this case. This change makes ironic-inspector log a warning for any item of incorrect length by default. The old behavior can be returned via [extra_hardware]strict. Also stops discarding the received data if parsing fails: maybe this data is not for us? Change-Id: I1af55fa4ec47b345479b9e5687ad480648e502ac
This commit is contained in:
parent
9ecb6e33eb
commit
3243cac3fc
@ -17,6 +17,7 @@ from ironic_inspector.conf import coordination
|
|||||||
from ironic_inspector.conf import default
|
from ironic_inspector.conf import default
|
||||||
from ironic_inspector.conf import discovery
|
from ironic_inspector.conf import discovery
|
||||||
from ironic_inspector.conf import dnsmasq_pxe_filter
|
from ironic_inspector.conf import dnsmasq_pxe_filter
|
||||||
|
from ironic_inspector.conf import extra_hardware
|
||||||
from ironic_inspector.conf import iptables
|
from ironic_inspector.conf import iptables
|
||||||
from ironic_inspector.conf import ironic
|
from ironic_inspector.conf import ironic
|
||||||
from ironic_inspector.conf import pci_devices
|
from ironic_inspector.conf import pci_devices
|
||||||
@ -35,6 +36,7 @@ coordination.register_opts(CONF)
|
|||||||
discovery.register_opts(CONF)
|
discovery.register_opts(CONF)
|
||||||
default.register_opts(CONF)
|
default.register_opts(CONF)
|
||||||
dnsmasq_pxe_filter.register_opts(CONF)
|
dnsmasq_pxe_filter.register_opts(CONF)
|
||||||
|
extra_hardware.register_opts(CONF)
|
||||||
iptables.register_opts(CONF)
|
iptables.register_opts(CONF)
|
||||||
ironic.register_opts(CONF)
|
ironic.register_opts(CONF)
|
||||||
pci_devices.register_opts(CONF)
|
pci_devices.register_opts(CONF)
|
||||||
|
33
ironic_inspector/conf/extra_hardware.py
Normal file
33
ironic_inspector/conf/extra_hardware.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from ironic_inspector.common.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
_OPTS = [
|
||||||
|
cfg.BoolOpt('strict',
|
||||||
|
default=False,
|
||||||
|
help=_('If True, refuse to parse extra data if at least one '
|
||||||
|
'record is too short. Additionally, remove the '
|
||||||
|
'incoming "data" even if parsing failed.')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_opts(_OPTS, group='extra_hardware')
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return _OPTS
|
@ -68,7 +68,7 @@ def list_opts():
|
|||||||
('discovery', ironic_inspector.conf.discovery.list_opts()),
|
('discovery', ironic_inspector.conf.discovery.list_opts()),
|
||||||
('dnsmasq_pxe_filter',
|
('dnsmasq_pxe_filter',
|
||||||
ironic_inspector.conf.dnsmasq_pxe_filter.list_opts()),
|
ironic_inspector.conf.dnsmasq_pxe_filter.list_opts()),
|
||||||
('swift', ironic_inspector.conf.swift.list_opts()),
|
('extra_hardware', ironic_inspector.conf.extra_hardware.list_opts()),
|
||||||
('ironic', ironic_inspector.conf.ironic.list_opts()),
|
('ironic', ironic_inspector.conf.ironic.list_opts()),
|
||||||
('iptables', ironic_inspector.conf.iptables.list_opts()),
|
('iptables', ironic_inspector.conf.iptables.list_opts()),
|
||||||
('port_physnet', ironic_inspector.conf.port_physnet.list_opts()),
|
('port_physnet', ironic_inspector.conf.port_physnet.list_opts()),
|
||||||
@ -76,4 +76,5 @@ def list_opts():
|
|||||||
('pci_devices', ironic_inspector.conf.pci_devices.list_opts()),
|
('pci_devices', ironic_inspector.conf.pci_devices.list_opts()),
|
||||||
('pxe_filter', ironic_inspector.conf.pxe_filter.list_opts()),
|
('pxe_filter', ironic_inspector.conf.pxe_filter.list_opts()),
|
||||||
('service_catalog', ironic_inspector.conf.service_catalog.list_opts()),
|
('service_catalog', ironic_inspector.conf.service_catalog.list_opts()),
|
||||||
|
('swift', ironic_inspector.conf.swift.list_opts()),
|
||||||
]
|
]
|
||||||
|
@ -18,11 +18,14 @@ string in a Swift object. The object is named 'extra_hardware-<node uuid>' and
|
|||||||
is stored in the 'inspector' container.
|
is stored in the 'inspector' container.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
from ironic_inspector.plugins import base
|
from ironic_inspector.plugins import base
|
||||||
from ironic_inspector import utils
|
from ironic_inspector import utils
|
||||||
|
|
||||||
LOG = utils.getProcessingLogger(__name__)
|
LOG = utils.getProcessingLogger(__name__)
|
||||||
EDEPLOY_ITEM_SIZE = 4
|
EDEPLOY_ITEM_SIZE = 4
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
class ExtraHardwareHook(base.ProcessingHook):
|
class ExtraHardwareHook(base.ProcessingHook):
|
||||||
@ -42,36 +45,31 @@ class ExtraHardwareHook(base.ProcessingHook):
|
|||||||
'the ramdisk', node_info=node_info,
|
'the ramdisk', node_info=node_info,
|
||||||
data=introspection_data)
|
data=introspection_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
data = introspection_data['data']
|
data = introspection_data['data']
|
||||||
|
if not self._is_edeploy_data(data):
|
||||||
|
LOG.warning('Extra hardware data was not in a recognised '
|
||||||
|
'format (eDeploy), and will not be forwarded to '
|
||||||
|
'introspection rules', node_info=node_info,
|
||||||
|
data=introspection_data)
|
||||||
|
if CONF.extra_hardware.strict:
|
||||||
|
LOG.debug('Deleting \"data\" key from introspection data as '
|
||||||
|
'it is malformed and strict mode is on.',
|
||||||
|
node_info=node_info, data=introspection_data)
|
||||||
|
del introspection_data['data']
|
||||||
|
return
|
||||||
|
|
||||||
# NOTE(sambetts) If data is edeploy format, convert to dicts for rules
|
# NOTE(sambetts) If data is edeploy format, convert to dicts for rules
|
||||||
# processing, store converted data in introspection_data['extra'].
|
# processing, store converted data in introspection_data['extra'].
|
||||||
# Delete introspection_data['data'], it is assumed unusable
|
# Delete introspection_data['data'], it is assumed unusable
|
||||||
# by rules.
|
# by rules.
|
||||||
if self._is_edeploy_data(data):
|
|
||||||
LOG.debug('Extra hardware data is in eDeploy format, '
|
|
||||||
'converting to usable format',
|
|
||||||
node_info=node_info, data=introspection_data)
|
|
||||||
introspection_data['extra'] = self._convert_edeploy_data(data)
|
|
||||||
else:
|
|
||||||
LOG.warning('Extra hardware data was not in a recognised '
|
|
||||||
'format (eDeploy), and will not be forwarded to '
|
|
||||||
'introspection rules', node_info=node_info,
|
|
||||||
data=introspection_data)
|
|
||||||
|
|
||||||
LOG.debug('Deleting \"data\" key from introspection data as it is '
|
|
||||||
'assumed unusable by introspection rules. Raw data is '
|
|
||||||
'stored in swift',
|
|
||||||
node_info=node_info, data=introspection_data)
|
|
||||||
del introspection_data['data']
|
|
||||||
|
|
||||||
def _is_edeploy_data(self, data):
|
|
||||||
return all(isinstance(item, list) and len(item) == EDEPLOY_ITEM_SIZE
|
|
||||||
for item in data)
|
|
||||||
|
|
||||||
def _convert_edeploy_data(self, data):
|
|
||||||
converted = {}
|
converted = {}
|
||||||
for item in data:
|
for item in data:
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
converted_0 = converted.setdefault(item[0], {})
|
converted_0 = converted.setdefault(item[0], {})
|
||||||
converted_1 = converted_0.setdefault(item[1], {})
|
converted_1 = converted_0.setdefault(item[1], {})
|
||||||
|
|
||||||
@ -81,4 +79,20 @@ class ExtraHardwareHook(base.ProcessingHook):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
converted_1[item[2]] = item[3]
|
converted_1[item[2]] = item[3]
|
||||||
return converted
|
except IndexError:
|
||||||
|
LOG.warning('Ignoring invalid extra data item %s', item,
|
||||||
|
node_info=node_info, data=introspection_data)
|
||||||
|
|
||||||
|
introspection_data['extra'] = converted
|
||||||
|
|
||||||
|
LOG.debug('Deleting \"data\" key from introspection data as it is '
|
||||||
|
'assumed unusable by introspection rules.',
|
||||||
|
node_info=node_info, data=introspection_data)
|
||||||
|
del introspection_data['data']
|
||||||
|
|
||||||
|
def _is_edeploy_data(self, data):
|
||||||
|
return isinstance(data, list) and all(
|
||||||
|
isinstance(item, list)
|
||||||
|
and (not CONF.extra_hardware.strict
|
||||||
|
or len(item) == EDEPLOY_ITEM_SIZE)
|
||||||
|
for item in data)
|
||||||
|
@ -11,14 +11,22 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
from ironic_inspector.plugins import extra_hardware
|
from ironic_inspector.plugins import extra_hardware
|
||||||
from ironic_inspector.test import base as test_base
|
from ironic_inspector.test import base as test_base
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(extra_hardware.LOG, 'warning', autospec=True)
|
||||||
class TestExtraHardware(test_base.NodeTest):
|
class TestExtraHardware(test_base.NodeTest):
|
||||||
hook = extra_hardware.ExtraHardwareHook()
|
hook = extra_hardware.ExtraHardwareHook()
|
||||||
|
|
||||||
def test_data_recieved(self):
|
def test_data_recieved(self, mock_warn):
|
||||||
introspection_data = {
|
introspection_data = {
|
||||||
'data': [['memory', 'total', 'size', '4294967296'],
|
'data': [['memory', 'total', 'size', '4294967296'],
|
||||||
['cpu', 'physical', 'number', '1'],
|
['cpu', 'physical', 'number', '1'],
|
||||||
@ -43,29 +51,63 @@ class TestExtraHardware(test_base.NodeTest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.assertEqual(expected, introspection_data['extra'])
|
self.assertEqual(expected, introspection_data['extra'])
|
||||||
|
self.assertFalse(mock_warn.called)
|
||||||
|
|
||||||
def test_data_not_in_edeploy_format(self):
|
def test_data_recieved_with_errors(self, mock_warn):
|
||||||
|
introspection_data = {
|
||||||
|
'data': [['memory', 'total', 'size', '4294967296'],
|
||||||
|
[],
|
||||||
|
['cpu', 'physical', 'number', '1'],
|
||||||
|
['cpu', 'physical', 'WUT'],
|
||||||
|
['cpu', 'logical', 'number', '1']]}
|
||||||
|
self.hook.before_processing(introspection_data)
|
||||||
|
self.hook.before_update(introspection_data, self.node_info)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'memory': {
|
||||||
|
'total': {
|
||||||
|
'size': 4294967296
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'cpu': {
|
||||||
|
'physical': {
|
||||||
|
'number': 1
|
||||||
|
},
|
||||||
|
'logical': {
|
||||||
|
'number': 1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(expected, introspection_data['extra'])
|
||||||
|
# An empty list is not a warning, a bad record is.
|
||||||
|
self.assertEqual(1, mock_warn.call_count)
|
||||||
|
|
||||||
|
def test_data_not_in_edeploy_format(self, mock_warn):
|
||||||
introspection_data = {
|
introspection_data = {
|
||||||
'data': [['memory', 'total', 'size', '4294967296'],
|
'data': [['memory', 'total', 'size', '4294967296'],
|
||||||
['cpu', 'physical', 'number', '1'],
|
['cpu', 'physical', 'number', '1'],
|
||||||
{'interface': 'eth1'}]}
|
{'interface': 'eth1'}]}
|
||||||
self.hook.before_processing(introspection_data)
|
self.hook.before_processing(introspection_data)
|
||||||
self.hook.before_update(introspection_data, self.node_info)
|
self.hook.before_update(introspection_data, self.node_info)
|
||||||
|
self.assertNotIn('extra', introspection_data)
|
||||||
|
self.assertIn('data', introspection_data)
|
||||||
|
self.assertTrue(mock_warn.called)
|
||||||
|
|
||||||
|
def test_data_not_in_edeploy_format_strict_mode(self, mock_warn):
|
||||||
|
CONF.set_override('strict', True, group='extra_hardware')
|
||||||
|
introspection_data = {
|
||||||
|
'data': [['memory', 'total', 'size', '4294967296'],
|
||||||
|
['cpu', 'physical', 'WUT']]
|
||||||
|
}
|
||||||
|
self.hook.before_processing(introspection_data)
|
||||||
|
self.hook.before_update(introspection_data, self.node_info)
|
||||||
|
self.assertNotIn('extra', introspection_data)
|
||||||
self.assertNotIn('data', introspection_data)
|
self.assertNotIn('data', introspection_data)
|
||||||
|
self.assertTrue(mock_warn.called)
|
||||||
|
|
||||||
def test_no_data_recieved(self):
|
def test_no_data_recieved(self, mock_warn):
|
||||||
introspection_data = {'cats': 'meow'}
|
introspection_data = {'cats': 'meow'}
|
||||||
self.hook.before_processing(introspection_data)
|
self.hook.before_processing(introspection_data)
|
||||||
self.hook.before_update(introspection_data, self.node_info)
|
self.hook.before_update(introspection_data, self.node_info)
|
||||||
|
self.assertTrue(mock_warn.called)
|
||||||
def test__convert_edeploy_data(self):
|
|
||||||
introspection_data = [['Sheldon', 'J.', 'Plankton', '123'],
|
|
||||||
['Larry', 'the', 'Lobster', None],
|
|
||||||
['Eugene', 'H.', 'Krabs', 'The cashier']]
|
|
||||||
|
|
||||||
data = self.hook._convert_edeploy_data(introspection_data)
|
|
||||||
expected_data = {'Sheldon': {'J.': {'Plankton': 123}},
|
|
||||||
'Larry': {'the': {'Lobster': None}},
|
|
||||||
'Eugene': {'H.': {'Krabs': 'The cashier'}}}
|
|
||||||
self.assertEqual(expected_data, data)
|
|
||||||
|
16
releasenotes/notes/extra-check-9cf0a7d89e534ccd.yaml
Normal file
16
releasenotes/notes/extra-check-9cf0a7d89e534ccd.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
The ``extra_hardware`` processing hook no longer refuses to parse extra
|
||||||
|
data if some records are empty or have unexpected length. These records
|
||||||
|
are now discared.
|
||||||
|
|
||||||
|
The previous behavior can be returned by setting the new option
|
||||||
|
``[extra_hardware]strict`` to ``True``.
|
||||||
|
- |
|
||||||
|
The ``extra_hardware`` processing hook no longer removes the incoming
|
||||||
|
``data`` object if it has unexpected data format, assuming that this
|
||||||
|
object is used for something else.
|
||||||
|
|
||||||
|
The previous behavior can be returned by setting the new option
|
||||||
|
``[extra_hardware]strict`` to ``True``.
|
Loading…
Reference in New Issue
Block a user