Merge "Add hardware manager interface for hardware initialization"
This commit is contained in:
commit
c688b98917
@ -289,6 +289,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
|
|||||||
|
|
||||||
# Cached hw managers at runtime, not load time. See bug 1490008.
|
# Cached hw managers at runtime, not load time. See bug 1490008.
|
||||||
hardware.load_managers()
|
hardware.load_managers()
|
||||||
|
# Request the hw manager to do long initializations
|
||||||
|
hardware.dispatch_to_managers('initialize_hardware')
|
||||||
|
|
||||||
if not self.standalone:
|
if not self.standalone:
|
||||||
# Inspection should be started before call to lookup, otherwise
|
# Inspection should be started before call to lookup, otherwise
|
||||||
|
@ -16,6 +16,7 @@ import abc
|
|||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
|
import time
|
||||||
|
|
||||||
import netifaces
|
import netifaces
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
@ -38,6 +39,9 @@ UNIT_CONVERTER = pint.UnitRegistry(filename=None)
|
|||||||
UNIT_CONVERTER.define('MB = []')
|
UNIT_CONVERTER.define('MB = []')
|
||||||
UNIT_CONVERTER.define('GB = 1024 MB')
|
UNIT_CONVERTER.define('GB = 1024 MB')
|
||||||
|
|
||||||
|
_DISK_WAIT_ATTEMPTS = 5
|
||||||
|
_DISK_WAIT_DELAY = 3
|
||||||
|
|
||||||
|
|
||||||
def _get_device_vendor(dev):
|
def _get_device_vendor(dev):
|
||||||
"""Get the vendor name of a given device."""
|
"""Get the vendor name of a given device."""
|
||||||
@ -385,17 +389,57 @@ class HardwareManager(object):
|
|||||||
'version': getattr(self, 'HARDWARE_MANAGER_VERSION', '1.0')
|
'version': getattr(self, 'HARDWARE_MANAGER_VERSION', '1.0')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def initialize_hardware(self):
|
||||||
|
"""Initialize hardware on the agent start up.
|
||||||
|
|
||||||
|
This method will be called once on start up before any calls
|
||||||
|
to list_hardware_info are made.
|
||||||
|
|
||||||
|
The default implementation does nothing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class GenericHardwareManager(HardwareManager):
|
class GenericHardwareManager(HardwareManager):
|
||||||
HARDWARE_MANAGER_NAME = 'generic_hardware_manager'
|
HARDWARE_MANAGER_NAME = 'generic_hardware_manager'
|
||||||
HARDWARE_MANAGER_VERSION = '1.0'
|
HARDWARE_MANAGER_VERSION = '1.0'
|
||||||
|
|
||||||
|
# These modules are rarely loaded automatically
|
||||||
|
_PRELOADED_MODULES = ['ipmi_msghandler', 'ipmi_devintf', 'ipmi_si']
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.sys_path = '/sys'
|
self.sys_path = '/sys'
|
||||||
|
|
||||||
def evaluate_hardware_support(self):
|
def evaluate_hardware_support(self):
|
||||||
return HardwareSupport.GENERIC
|
return HardwareSupport.GENERIC
|
||||||
|
|
||||||
|
def initialize_hardware(self):
|
||||||
|
LOG.debug('Initializing hardware')
|
||||||
|
self._preload_modules()
|
||||||
|
_udev_settle()
|
||||||
|
self._wait_for_disks()
|
||||||
|
|
||||||
|
def _preload_modules(self):
|
||||||
|
# TODO(dtantsur): try to load as many kernel modules for present
|
||||||
|
# hardware as it's possible.
|
||||||
|
for mod in self._PRELOADED_MODULES:
|
||||||
|
utils.try_execute('modprobe', mod)
|
||||||
|
|
||||||
|
def _wait_for_disks(self):
|
||||||
|
# Wait for at least one suitable disk to show up, otherwise neither
|
||||||
|
# inspection not deployment have any chances to succeed.
|
||||||
|
for attempt in range(_DISK_WAIT_ATTEMPTS):
|
||||||
|
try:
|
||||||
|
self.get_os_install_device()
|
||||||
|
except errors.DeviceNotFound:
|
||||||
|
LOG.debug('Still waiting for at least one disk to appear, '
|
||||||
|
'attempt %d of %d', attempt + 1, _DISK_WAIT_ATTEMPTS)
|
||||||
|
time.sleep(_DISK_WAIT_DELAY)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
LOG.warning('No disks detected in %d seconds',
|
||||||
|
_DISK_WAIT_DELAY * _DISK_WAIT_ATTEMPTS)
|
||||||
|
|
||||||
def _get_interface_info(self, interface_name):
|
def _get_interface_info(self, interface_name):
|
||||||
addr_path = '{0}/class/net/{1}/address'.format(self.sys_path,
|
addr_path = '{0}/class/net/{1}/address'.format(self.sys_path,
|
||||||
interface_name)
|
interface_name)
|
||||||
@ -738,11 +782,6 @@ class GenericHardwareManager(HardwareManager):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_bmc_address(self):
|
def get_bmc_address(self):
|
||||||
# These modules are rarely loaded automatically
|
|
||||||
utils.try_execute('modprobe', 'ipmi_msghandler')
|
|
||||||
utils.try_execute('modprobe', 'ipmi_devintf')
|
|
||||||
utils.try_execute('modprobe', 'ipmi_si')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
out, _e = utils.execute(
|
out, _e = utils.execute(
|
||||||
"ipmitool lan print | grep -e 'IP Address [^S]' "
|
"ipmitool lan print | grep -e 'IP Address [^S]' "
|
||||||
|
@ -167,9 +167,12 @@ class TestBaseAgent(test_base.BaseTestCase):
|
|||||||
self.assertEqual(pkg_resources.get_distribution('ironic-python-agent')
|
self.assertEqual(pkg_resources.get_distribution('ironic-python-agent')
|
||||||
.version, status.version)
|
.version, status.version)
|
||||||
|
|
||||||
|
@mock.patch.object(hardware.GenericHardwareManager, 'initialize_hardware',
|
||||||
|
autospec=True)
|
||||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||||
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info')
|
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info')
|
||||||
def test_run(self, mocked_list_hardware, wsgi_server_cls):
|
def test_run(self, mocked_list_hardware, wsgi_server_cls,
|
||||||
|
mocked_init_hardware):
|
||||||
CONF.set_override('inspection_callback_url', '', enforce_type=True)
|
CONF.set_override('inspection_callback_url', '', enforce_type=True)
|
||||||
wsgi_server = wsgi_server_cls.return_value
|
wsgi_server = wsgi_server_cls.return_value
|
||||||
wsgi_server.start.side_effect = KeyboardInterrupt()
|
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||||
@ -193,12 +196,15 @@ class TestBaseAgent(test_base.BaseTestCase):
|
|||||||
wsgi_server.serve_forever.assert_called_once_with()
|
wsgi_server.serve_forever.assert_called_once_with()
|
||||||
|
|
||||||
self.agent.heartbeater.start.assert_called_once_with()
|
self.agent.heartbeater.start.assert_called_once_with()
|
||||||
|
mocked_init_hardware.assert_called_once_with(mock.ANY)
|
||||||
|
|
||||||
@mock.patch.object(inspector, 'inspect', autospec=True)
|
@mock.patch.object(inspector, 'inspect', autospec=True)
|
||||||
|
@mock.patch.object(hardware.GenericHardwareManager, 'initialize_hardware',
|
||||||
|
autospec=True)
|
||||||
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||||
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info')
|
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info')
|
||||||
def test_run_with_inspection(self, mocked_list_hardware, wsgi_server_cls,
|
def test_run_with_inspection(self, mocked_list_hardware, wsgi_server_cls,
|
||||||
mocked_inspector):
|
mocked_init_hardware, mocked_inspector):
|
||||||
CONF.set_override('inspection_callback_url', 'http://foo/bar',
|
CONF.set_override('inspection_callback_url', 'http://foo/bar',
|
||||||
enforce_type=True)
|
enforce_type=True)
|
||||||
|
|
||||||
@ -231,6 +237,7 @@ class TestBaseAgent(test_base.BaseTestCase):
|
|||||||
self.agent.api_client.lookup_node.call_args[1]['node_uuid'])
|
self.agent.api_client.lookup_node.call_args[1]['node_uuid'])
|
||||||
|
|
||||||
self.agent.heartbeater.start.assert_called_once_with()
|
self.agent.heartbeater.start.assert_called_once_with()
|
||||||
|
mocked_init_hardware.assert_called_once_with(mock.ANY)
|
||||||
|
|
||||||
def test_async_command_success(self):
|
def test_async_command_success(self):
|
||||||
result = base.AsyncCommandResult('foo_command', {'fail': False},
|
result = base.AsyncCommandResult('foo_command', {'fail': False},
|
||||||
|
@ -12,9 +12,11 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import netifaces
|
import netifaces
|
||||||
import os
|
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_utils import units
|
from oslo_utils import units
|
||||||
from oslotest import base as test_base
|
from oslotest import base as test_base
|
||||||
@ -1085,6 +1087,66 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
|
|||||||
self.hardware.get_system_vendor_info().manufacturer)
|
self.hardware.get_system_vendor_info().manufacturer)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(time, 'sleep', autospec=True)
|
||||||
|
@mock.patch.object(hardware.GenericHardwareManager,
|
||||||
|
'get_os_install_device', autospec=True)
|
||||||
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
|
class TestGenericHardwareManagerInitializeHardware(test_base.BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestGenericHardwareManagerInitializeHardware, self).setUp()
|
||||||
|
self.hardware = hardware.GenericHardwareManager()
|
||||||
|
|
||||||
|
def test_ok(self, mocked_execute, mocked_os_dev, mocked_sleep):
|
||||||
|
self.hardware.initialize_hardware()
|
||||||
|
|
||||||
|
expected_execute_calls = [
|
||||||
|
mock.call('modprobe', mod)
|
||||||
|
for mod in self.hardware._PRELOADED_MODULES
|
||||||
|
]
|
||||||
|
expected_execute_calls.append(mock.call('udevadm', 'settle'))
|
||||||
|
self.assertEqual(expected_execute_calls, mocked_execute.call_args_list)
|
||||||
|
mocked_os_dev.assert_called_once_with(mock.ANY)
|
||||||
|
self.assertFalse(mocked_sleep.called)
|
||||||
|
|
||||||
|
def test_disk_delayed(self, mocked_execute, mocked_os_dev, mocked_sleep):
|
||||||
|
mocked_os_dev.side_effect = [
|
||||||
|
errors.DeviceNotFound(''),
|
||||||
|
errors.DeviceNotFound(''),
|
||||||
|
None
|
||||||
|
]
|
||||||
|
|
||||||
|
self.hardware.initialize_hardware()
|
||||||
|
|
||||||
|
expected_execute_calls = [
|
||||||
|
mock.call('modprobe', mod)
|
||||||
|
for mod in self.hardware._PRELOADED_MODULES
|
||||||
|
]
|
||||||
|
expected_execute_calls.append(mock.call('udevadm', 'settle'))
|
||||||
|
self.assertEqual(expected_execute_calls, mocked_execute.call_args_list)
|
||||||
|
mocked_os_dev.assert_called_with(mock.ANY)
|
||||||
|
self.assertEqual(3, mocked_os_dev.call_count)
|
||||||
|
self.assertEqual(2, mocked_sleep.call_count)
|
||||||
|
|
||||||
|
@mock.patch.object(hardware.LOG, 'warning', autospec=True)
|
||||||
|
def test_no_disk(self, mocked_warn, mocked_execute, mocked_os_dev,
|
||||||
|
mocked_sleep):
|
||||||
|
mocked_os_dev.side_effect = errors.DeviceNotFound('')
|
||||||
|
|
||||||
|
self.hardware.initialize_hardware()
|
||||||
|
|
||||||
|
expected_execute_calls = [
|
||||||
|
mock.call('modprobe', mod)
|
||||||
|
for mod in self.hardware._PRELOADED_MODULES
|
||||||
|
]
|
||||||
|
expected_execute_calls.append(mock.call('udevadm', 'settle'))
|
||||||
|
self.assertEqual(expected_execute_calls, mocked_execute.call_args_list)
|
||||||
|
mocked_os_dev.assert_called_with(mock.ANY)
|
||||||
|
self.assertEqual(hardware._DISK_WAIT_ATTEMPTS,
|
||||||
|
mocked_os_dev.call_count)
|
||||||
|
self.assertEqual(hardware._DISK_WAIT_ATTEMPTS, mocked_sleep.call_count)
|
||||||
|
self.assertTrue(mocked_warn.called)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', autospec=True)
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
class TestModuleFunctions(test_base.BaseTestCase):
|
class TestModuleFunctions(test_base.BaseTestCase):
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Add a new hardware manager method "initialize_hardware" which can be
|
||||||
|
overriden to provide early oneshot hardware initialization, such as loading
|
||||||
|
kernel modules or probing hardware. The generic implementation loads the
|
||||||
|
IPMI modules, calls "udev settle" and waits for at least one suitable disk
|
||||||
|
to appear in the "lsblk" output.
|
Loading…
x
Reference in New Issue
Block a user