Skip to content

Commit e9c0578

Browse files
committed
Call evaluate_hardware_support exactly once per hwm
Fixes an issue where we could call evaluate_hardware_support multiple times each run. Now, instead, we cache the values and use the cache where needed. Adds unit test coverage for get_managers and the new method. Fixes issue where we were caching hardware managers between unit tests. Closes-bug: 2066308 Change-Id: Iebc5b6d2440bfc9f23daa322493379bbe69e84d0 (cherry picked from commit c39517b)
1 parent c1581df commit e9c0578

File tree

7 files changed

+148
-58
lines changed

7 files changed

+148
-58
lines changed

ironic_python_agent/hardware.py

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3148,46 +3148,65 @@ def _collect_udev(io_dict):
31483148
io_dict[f'udev/{kname}'] = buf
31493149

31503150

3151-
def _compare_extensions(ext1, ext2):
3152-
mgr1 = ext1.obj
3153-
mgr2 = ext2.obj
3154-
return mgr2.evaluate_hardware_support() - mgr1.evaluate_hardware_support()
3151+
def _compare_managers(hwm1, hwm2):
3152+
return hwm2['support'] - hwm1['support']
3153+
3154+
3155+
def _get_extensions():
3156+
return stevedore.ExtensionManager(
3157+
namespace='ironic_python_agent.hardware_managers',
3158+
invoke_on_load=True
3159+
)
31553160

31563161

31573162
def get_managers():
31583163
"""Get a list of hardware managers in priority order.
31593164
3160-
Use stevedore to find all eligible hardware managers, sort them based on
3161-
self-reported (via evaluate_hardware_support()) priorities, and return them
3162-
in a list. The resulting list is cached in _global_managers.
3165+
This exists as a backwards compatibility shim, returning a simple list
3166+
of managers where expected. New usages should use get_managers_detail.
31633167
31643168
:returns: Priority-sorted list of hardware managers
31653169
:raises HardwareManagerNotFound: if no valid hardware managers found
31663170
"""
3171+
return [hwm['manager'] for hwm in get_managers_detail()]
3172+
3173+
3174+
def get_managers_detail():
3175+
"""Get detailed information about hardware managers
3176+
3177+
Use stevedore to find all eligible hardware managers, sort them based on
3178+
self-reported (via evaluate_hardware_support()) priorities, and return a
3179+
dict containing the manager object, it's class name, and hardware support
3180+
value. The resulting list is cached in _global_managers.
3181+
3182+
:returns: list of dictionaries representing hardware managers and metadata
3183+
:raises HardwareManagerNotFound: if no valid hardware managers found
3184+
"""
31673185
global _global_managers
31683186

31693187
if not _global_managers:
3170-
extension_manager = stevedore.ExtensionManager(
3171-
namespace='ironic_python_agent.hardware_managers',
3172-
invoke_on_load=True)
3173-
3174-
# There will always be at least one extension available (the
3175-
# GenericHardwareManager).
3176-
extensions = sorted(extension_manager,
3177-
key=functools.cmp_to_key(_compare_extensions))
31783188

31793189
preferred_managers = []
31803190

3181-
for extension in extensions:
3182-
if extension.obj.evaluate_hardware_support() > 0:
3183-
preferred_managers.append(extension.obj)
3191+
for extension in _get_extensions():
3192+
hwm = extension.obj
3193+
hardware_support = hwm.evaluate_hardware_support()
3194+
if hardware_support > 0:
3195+
preferred_managers.append({
3196+
'name': hwm.__class__.__name__,
3197+
'manager': hwm,
3198+
'support': hardware_support
3199+
})
31843200
LOG.info('Hardware manager found: %s',
31853201
extension.entry_point_target)
31863202

31873203
if not preferred_managers:
31883204
raise errors.HardwareManagerNotFound
31893205

3190-
_global_managers = preferred_managers
3206+
hwms = sorted(preferred_managers,
3207+
key=functools.cmp_to_key(_compare_managers))
3208+
3209+
_global_managers = hwms
31913210

31923211
return _global_managers
31933212

@@ -3381,20 +3400,14 @@ def deduplicate_steps(candidate_steps):
33813400
all managers, key=manager, value=list of steps
33823401
:returns: A deduplicated dictionary of {hardware_manager: [steps]}
33833402
"""
3384-
support = dispatch_to_all_managers(
3385-
'evaluate_hardware_support')
3403+
support = {hwm['name']: hwm['support']
3404+
for hwm in get_managers_detail()}
33863405

33873406
steps = collections.defaultdict(list)
33883407
deduped_steps = collections.defaultdict(list)
33893408

33903409
for manager, manager_steps in candidate_steps.items():
33913410
# We cannot deduplicate steps with unknown hardware support
3392-
if manager not in support:
3393-
LOG.warning('Unknown hardware support for %(manager)s, '
3394-
'dropping steps: %(steps)s',
3395-
{'manager': manager, 'steps': manager_steps})
3396-
continue
3397-
33983411
for step in manager_steps:
33993412
# build a new dict of steps that's easier to filter
34003413
step['hwm'] = {'name': manager,

ironic_python_agent/tests/unit/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def setUp(self):
6363

6464
ext_base._EXT_MANAGER = None
6565
hardware._CACHED_HW_INFO = None
66+
hardware._global_managers = None
6667

6768
def _set_config(self):
6869
self.cfg_fixture = self.useFixture(config_fixture.Config(CONF))

ironic_python_agent/tests/unit/extensions/test_clean.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ def setUp(self):
3434
}
3535
self.version = {'generic': '1', 'specific': '1'}
3636

37+
@mock.patch('ironic_python_agent.hardware.get_managers_detail',
38+
autospec=True)
3739
@mock.patch('ironic_python_agent.hardware.get_current_versions',
3840
autospec=True)
3941
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
4042
autospec=True)
4143
def test_get_clean_steps(self, mock_dispatch, mock_version,
42-
mock_cache_node):
44+
mock_managers, mock_cache_node):
4345
mock_version.return_value = self.version
4446

4547
manager_steps = {
@@ -118,13 +120,17 @@ def test_get_clean_steps(self, mock_dispatch, mock_version,
118120

119121
}
120122

121-
hardware_support = {
122-
'SpecificHardwareManager': 3,
123-
'FirmwareHardwareManager': 4,
124-
'DiskHardwareManager': 4
125-
}
123+
# NOTE(JayF): The real dict also has manager: hwm-object
124+
# but we don't use it in the code under test
125+
hwms = [
126+
{'name': 'SpecificHardwareManager', 'support': 3},
127+
{'name': 'FirmwareHardwareManager', 'support': 4},
128+
{'name': 'DiskHardwareManager', 'support': 4},
129+
]
130+
131+
mock_dispatch.side_effect = [manager_steps]
132+
mock_managers.return_value = hwms
126133

127-
mock_dispatch.side_effect = [manager_steps, hardware_support]
128134
expected_return = {
129135
'hardware_manager_version': self.version,
130136
'clean_steps': expected_steps

ironic_python_agent/tests/unit/extensions/test_deploy.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ def setUp(self):
3232
}
3333
self.version = {'generic': '1', 'specific': '1'}
3434

35+
@mock.patch('ironic_python_agent.hardware.get_managers_detail',
36+
autospec=True)
3537
@mock.patch('ironic_python_agent.hardware.get_current_versions',
3638
autospec=True)
3739
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
3840
autospec=True)
3941
def test_get_deploy_steps(self, mock_dispatch, mock_version,
40-
mock_cache_node):
42+
mock_managers, mock_cache_node):
4143
mock_version.return_value = self.version
4244

4345
manager_steps = {
@@ -116,13 +118,17 @@ def test_get_deploy_steps(self, mock_dispatch, mock_version,
116118

117119
}
118120

119-
hardware_support = {
120-
'SpecificHardwareManager': 3,
121-
'FirmwareHardwareManager': 4,
122-
'DiskHardwareManager': 4
123-
}
121+
# NOTE(JayF): The real dict also has manager: hwm-object
122+
# but we don't use it in the code under test
123+
hwms = [
124+
{'name': 'SpecificHardwareManager', 'support': 3},
125+
{'name': 'FirmwareHardwareManager', 'support': 4},
126+
{'name': 'DiskHardwareManager', 'support': 4},
127+
]
128+
129+
mock_dispatch.side_effect = [manager_steps]
130+
mock_managers.return_value = hwms
124131

125-
mock_dispatch.side_effect = [manager_steps, hardware_support]
126132
expected_return = {
127133
'hardware_manager_version': self.version,
128134
'deploy_steps': expected_steps

ironic_python_agent/tests/unit/extensions/test_service.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ def setUp(self):
3434
}
3535
self.version = {'generic': '1', 'specific': '1'}
3636

37+
@mock.patch('ironic_python_agent.hardware.get_managers_detail',
38+
autospec=True)
3739
@mock.patch('ironic_python_agent.hardware.get_current_versions',
3840
autospec=True)
3941
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
4042
autospec=True)
4143
def test_get_service_steps(self, mock_dispatch, mock_version,
42-
mock_cache_node):
44+
mock_managers, mock_cache_node):
4345
mock_version.return_value = self.version
4446

4547
manager_steps = {
@@ -118,13 +120,17 @@ def test_get_service_steps(self, mock_dispatch, mock_version,
118120

119121
}
120122

121-
hardware_support = {
122-
'SpecificHardwareManager': 3,
123-
'FirmwareHardwareManager': 4,
124-
'DiskHardwareManager': 4
125-
}
123+
# NOTE(JayF): The real dict also has manager: hwm-object
124+
# but we don't use it in the code under test
125+
hwms = [
126+
{'name': 'SpecificHardwareManager', 'support': 3},
127+
{'name': 'FirmwareHardwareManager', 'support': 4},
128+
{'name': 'DiskHardwareManager', 'support': 4},
129+
]
130+
131+
mock_dispatch.side_effect = [manager_steps]
132+
mock_managers.return_value = hwms
126133

127-
mock_dispatch.side_effect = [manager_steps, hardware_support]
128134
expected_return = {
129135
'hardware_manager_version': self.version,
130136
'service_steps': expected_steps

ironic_python_agent/tests/unit/test_hardware.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,22 @@
8181
]
8282

8383

84-
class FakeHardwareManager(hardware.GenericHardwareManager):
85-
def __init__(self, hardware_support):
86-
self._hardware_support = hardware_support
87-
84+
class FakeHardwareManager(hardware.HardwareManager):
8885
def evaluate_hardware_support(self):
89-
return self._hardware_support
86+
return self.support
87+
88+
89+
def _create_mock_hwm(name, support):
90+
def set_support(self, x):
91+
self.support = x
92+
93+
# note(JayF): This code creates a subclass of FakeHardwareManager with
94+
# a unique name. Since we actually use the class name in IPA
95+
# code as an identifier, we need to have a new class for each
96+
# mock.
97+
hwm = type(name, (FakeHardwareManager,), {'_set_support': set_support})()
98+
hwm._set_support(support)
99+
return hwm
90100

91101

92102
class TestHardwareManagerLoading(base.IronicAgentTest):
@@ -104,17 +114,58 @@ def setUp(self):
104114
fake_ep.attrs = ['fake attrs']
105115
ext1 = extension.Extension(
106116
'fake_generic0', fake_ep, None,
107-
FakeHardwareManager(hardware.HardwareSupport.GENERIC))
117+
_create_mock_hwm("fake_generic0",
118+
hardware.HardwareSupport.GENERIC))
108119
ext2 = extension.Extension(
109120
'fake_mainline0', fake_ep, None,
110-
FakeHardwareManager(hardware.HardwareSupport.MAINLINE))
121+
_create_mock_hwm("fake_mainline0",
122+
hardware.HardwareSupport.MAINLINE))
111123
ext3 = extension.Extension(
112-
'fake_generic1', fake_ep, None,
113-
FakeHardwareManager(hardware.HardwareSupport.GENERIC))
114-
self.correct_hw_manager = ext2.obj
124+
'fake_serviceprovider0', fake_ep, None,
125+
_create_mock_hwm("fake_serviceprovider0",
126+
hardware.HardwareSupport.SERVICE_PROVIDER))
127+
# Note(JayF): Ensure these are added in an order other than priority
128+
# order or else you may invalidate the entire test :)
115129
self.fake_ext_mgr = extension.ExtensionManager.make_test_instance([
116130
ext1, ext2, ext3
117131
])
132+
self.expected_detail_response = [
133+
{'name': 'fake_serviceprovider0',
134+
'support': hardware.HardwareSupport.SERVICE_PROVIDER,
135+
'manager': ext3.obj},
136+
{'name': 'fake_mainline0',
137+
'support': hardware.HardwareSupport.MAINLINE,
138+
'manager': ext2.obj},
139+
{'name': 'fake_generic0',
140+
'support': hardware.HardwareSupport.GENERIC,
141+
'manager': ext1.obj},
142+
]
143+
self.expected_get_managers_response = [ext3.obj, ext2.obj, ext1.obj]
144+
145+
@mock.patch.object(hardware, '_get_extensions', autospec=True)
146+
def test_get_managers(self, mock_extensions):
147+
"""Test to ensure get_managers sorts and returns a list of HWMs.
148+
149+
The most meaningful part of this test is ensuring HWMs are in priority
150+
order, with the highest hardware support value coming earlier in the
151+
list of classes.
152+
"""
153+
mock_extensions.return_value = self.fake_ext_mgr
154+
expected_names = [x.__class__.__name__
155+
for x in self.expected_get_managers_response]
156+
actual_names = [x.__class__.__name__
157+
for x in hardware.get_managers()]
158+
self.assertEqual(actual_names, expected_names)
159+
160+
@mock.patch.object(hardware, '_get_extensions', autospec=True)
161+
def test_get_managers_detail(self, mock_extensions):
162+
"""ensure get_manager_details returns a list of HWMs + metadata
163+
164+
These also need to be sorted in priority order
165+
"""
166+
mock_extensions.return_value = self.fake_ext_mgr
167+
self.assertEqual(hardware.get_managers_detail(),
168+
self.expected_detail_response)
118169

119170

120171
@mock.patch.object(hardware, '_udev_settle', lambda *_: None)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
fixes:
3+
- |
4+
Fixes bug 2066308, an issue where Ironic Python Agent would call
5+
evaluate_hardware_support multiple times on hardware manager plugins.
6+
Scanning for hardware and disks is time consuming, and caused timeouts
7+
on badly-performing nodes.

0 commit comments

Comments
 (0)