When boot option is not persisted, set boot on next power on

If the server ipmi does not support the command 'chassis bootdev'
with 'persistent' option', the boot device cannot be set persistently
and will not be set for the 2nd boot.  This change proposes to add
a new parameter in node's driver_info to force set the boot device for
each power on.

Change-Id: I8d70c6292e3e013ccaf90f9ce4a616c8c916fe64
Closes-Bug: #1407820
This commit is contained in:
Zhenguo Niu 2015-04-27 11:41:27 +08:00
parent ea09be5605
commit cf9466d9ac
7 changed files with 285 additions and 5 deletions

View File

@ -37,6 +37,7 @@ from ironic.common import utils
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules import console_utils
from ironic.drivers import utils as driver_utils
pyghmi = importutils.try_import('pyghmi')
if pyghmi:
@ -68,7 +69,16 @@ LOG = logging.getLogger(__name__)
REQUIRED_PROPERTIES = {'ipmi_address': _("IP of the node's BMC. Required."),
'ipmi_password': _("IPMI password. Required."),
'ipmi_username': _("IPMI username. Required.")}
COMMON_PROPERTIES = REQUIRED_PROPERTIES
OPTIONAL_PROPERTIES = {
'ipmi_force_boot_device': _("Whether Ironic should specify the boot "
"device to the BMC each time the server "
"is turned on, eg. because the BMC is not "
"capable of remembering the selected boot "
"device across power cycles; default value "
"is False. Optional.")
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
CONSOLE_PROPERTIES = {
'ipmi_terminal_port': _("node's UDP port to connect to. Only required for "
"console access.")
@ -102,6 +112,7 @@ def _parse_driver_info(node):
bmc_info['address'] = info.get('ipmi_address')
bmc_info['username'] = info.get('ipmi_username')
bmc_info['password'] = info.get('ipmi_password')
bmc_info['force_boot_device'] = info.get('ipmi_force_boot_device', False)
# get additional info
bmc_info['uuid'] = node.uuid
@ -337,6 +348,7 @@ class NativeIPMIPower(base.PowerInterface):
driver_info = _parse_driver_info(task.node)
if pstate == states.POWER_ON:
driver_utils.ensure_next_boot_device(task, driver_info)
_power_on(driver_info)
elif pstate == states.POWER_OFF:
_power_off(driver_info)
@ -358,6 +370,7 @@ class NativeIPMIPower(base.PowerInterface):
"""
driver_info = _parse_driver_info(task.node)
driver_utils.ensure_next_boot_device(task, driver_info)
_reboot(driver_info)
@ -409,6 +422,15 @@ class NativeIPMIManagement(base.ManagementInterface):
if device not in self.get_supported_boot_devices():
raise exception.InvalidParameterValue(_(
"Invalid boot device %s specified.") % device)
if task.node.driver_info.get('ipmi_force_boot_device', False):
driver_utils.force_persistent_boot(task,
device,
persistent)
# Reset persistent to False, in case of BMC does not support
# persistent or we do not have admin rights.
persistent = False
driver_info = _parse_driver_info(task.node)
try:
ipmicmd = ipmi_command.Command(bmc=driver_info['address'],
@ -439,8 +461,19 @@ class NativeIPMIManagement(base.ManagementInterface):
future boots or not, None if it is unknown.
"""
driver_info = task.node.driver_info
driver_internal_info = task.node.driver_internal_info
if (driver_info.get('ipmi_force_boot_device', False) and
driver_internal_info.get('persistent_boot_device') and
driver_internal_info.get('is_next_boot_persistent', True)):
return {
'boot_device': driver_internal_info['persistent_boot_device'],
'persistent': True
}
driver_info = _parse_driver_info(task.node)
response = {'boot_device': None}
try:
ipmicmd = ipmi_command.Command(bmc=driver_info['address'],
userid=driver_info['username'],

View File

@ -51,6 +51,7 @@ from ironic.common import utils
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules import console_utils
from ironic.drivers import utils as driver_utils
CONF = cfg.CONF
@ -92,6 +93,12 @@ OPTIONAL_PROPERTIES = {
"to \"single\" or \"dual\". Optional."),
'ipmi_protocol_version': _('the version of the IPMI protocol; default '
'is "2.0". One of "1.5", "2.0". Optional.'),
'ipmi_force_boot_device': _("Whether Ironic should specify the boot "
"device to the BMC each time the server "
"is turned on, eg. because the BMC is not "
"capable of remembering the selected boot "
"device across power cycles; default value "
"is False. Optional.")
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
@ -247,6 +254,7 @@ def _parse_driver_info(node):
target_channel = info.get('ipmi_target_channel')
target_address = info.get('ipmi_target_address')
protocol_version = str(info.get('ipmi_protocol_version', '2.0'))
force_boot_device = info.get('ipmi_force_boot_device', False)
if protocol_version not in VALID_PROTO_VERSIONS:
valid_versions = ', '.join(VALID_PROTO_VERSIONS)
@ -322,6 +330,7 @@ def _parse_driver_info(node):
'target_channel': target_channel,
'target_address': target_address,
'protocol_version': protocol_version,
'force_boot_device': force_boot_device,
}
@ -722,6 +731,7 @@ class IPMIPower(base.PowerInterface):
driver_info = _parse_driver_info(task.node)
if pstate == states.POWER_ON:
driver_utils.ensure_next_boot_device(task, driver_info)
state = _power_on(driver_info)
elif pstate == states.POWER_OFF:
state = _power_off(driver_info)
@ -746,6 +756,7 @@ class IPMIPower(base.PowerInterface):
"""
driver_info = _parse_driver_info(task.node)
_power_off(driver_info)
driver_utils.ensure_next_boot_device(task, driver_info)
state = _power_on(driver_info)
if state != states.POWER_ON:
@ -820,6 +831,14 @@ class IPMIManagement(base.ManagementInterface):
timeout_disable = "0x00 0x08 0x03 0x08"
send_raw(task, timeout_disable)
if task.node.driver_info.get('ipmi_force_boot_device', False):
driver_utils.force_persistent_boot(task,
device,
persistent)
# Reset persistent to False, in case of BMC does not support
# persistent or we do not have admin rights.
persistent = False
cmd = "chassis bootdev %s" % device
if persistent:
cmd = cmd + " options=persistent"
@ -852,9 +871,21 @@ class IPMIManagement(base.ManagementInterface):
future boots or not, None if it is unknown.
"""
driver_info = task.node.driver_info
driver_internal_info = task.node.driver_internal_info
if (driver_info.get('ipmi_force_boot_device', False) and
driver_internal_info.get('persistent_boot_device') and
driver_internal_info.get('is_next_boot_persistent', True)):
return {
'boot_device': driver_internal_info['persistent_boot_device'],
'persistent': True
}
cmd = "chassis bootparam get 5"
driver_info = _parse_driver_info(task.node)
response = {'boot_device': None, 'persistent': None}
try:
out, err = _exec_ipmitool(driver_info, cmd)
except (exception.PasswordFileFailedToCreate,

View File

@ -17,6 +17,7 @@ from oslo_log import log as logging
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LW
from ironic.conductor import utils
from ironic.drivers import base
@ -170,3 +171,48 @@ def add_node_capability(task, capability, value):
properties['capabilities'] = capabilities
node.properties = properties
node.save()
def ensure_next_boot_device(task, driver_info):
"""Ensure boot from correct device if persistent is True
If ipmi_force_boot_device is True and is_next_boot_persistent, set to
boot from correct device, else unset is_next_boot_persistent field.
:param task: Node object.
:param driver_info: Node driver_info.
"""
if driver_info.get('force_boot_device', False):
driver_internal_info = task.node.driver_internal_info
if driver_internal_info.get('is_next_boot_persistent') is False:
driver_internal_info.pop('is_next_boot_persistent', None)
task.node.driver_internal_info = driver_internal_info
task.node.save()
else:
boot_device = driver_internal_info.get('persistent_boot_device')
if boot_device:
utils.node_set_boot_device(task, boot_device)
def force_persistent_boot(task, device, persistent):
"""Set persistent boot device to driver_internal_info
If persistent is True set 'persistent_boot_device' field to the
boot device and reset persistent to False, else set
'is_next_boot_persistent' to False.
:param task: Task object.
:param device: Boot device.
:param persistent: Whether next boot is persistent or not.
"""
node = task.node
driver_internal_info = node.driver_internal_info
if persistent:
driver_internal_info['persistent_boot_device'] = device
else:
driver_internal_info['is_next_boot_persistent'] = False
node.driver_internal_info = driver_internal_info
node.save()

View File

@ -3299,12 +3299,13 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
'ipmi_transit_channel', 'ipmi_transit_address',
'ipmi_target_channel', 'ipmi_target_address',
'ipmi_local_address', 'ipmi_protocol_version',
'ipmi_force_boot_device'
]
self._check_driver_properties("fake_ipmitool", expected)
def test_driver_properties_fake_ipminative(self):
expected = ['ipmi_address', 'ipmi_password', 'ipmi_username',
'ipmi_terminal_port']
'ipmi_terminal_port', 'ipmi_force_boot_device']
self._check_driver_properties("fake_ipminative", expected)
def test_driver_properties_fake_ssh(self):
@ -3335,13 +3336,14 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
'ipmi_transit_address', 'ipmi_target_channel',
'ipmi_target_address', 'ipmi_local_address',
'deploy_kernel', 'deploy_ramdisk', 'ipmi_protocol_version',
'ipmi_force_boot_device'
]
self._check_driver_properties("pxe_ipmitool", expected)
def test_driver_properties_pxe_ipminative(self):
expected = ['ipmi_address', 'ipmi_password', 'ipmi_username',
'deploy_kernel', 'deploy_ramdisk',
'ipmi_terminal_port']
'ipmi_terminal_port', 'ipmi_force_boot_device']
self._check_driver_properties("pxe_ipminative", expected)
def test_driver_properties_pxe_ssh(self):

View File

@ -30,6 +30,7 @@ from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers.modules import console_utils
from ironic.drivers.modules import ipminative
from ironic.drivers import utils as driver_utils
from ironic.tests.conductor import utils as mgr_utils
from ironic.tests.db import base as db_base
from ironic.tests.db import utils as db_utils
@ -54,6 +55,7 @@ class IPMINativePrivateMethodTestCase(db_base.DbTestCase):
self.assertIsNotNone(self.info.get('username'))
self.assertIsNotNone(self.info.get('password'))
self.assertIsNotNone(self.info.get('uuid'))
self.assertIsNotNone(self.info.get('force_boot_device'))
# make sure error is raised when info, eg. username, is missing
info = dict(INFO_DICT)
@ -263,6 +265,18 @@ class IPMINativeDriverTestCase(db_base.DbTestCase):
task, states.POWER_ON)
power_on_mock.assert_called_once_with(self.info)
@mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True)
@mock.patch.object(ipminative, '_power_on', autospec=True)
def test_set_power_on_with_next_boot(self, power_on_mock, mock_next_boot):
power_on_mock.return_value = states.POWER_ON
with task_manager.acquire(self.context,
self.node.uuid) as task:
self.driver.power.set_power_state(
task, states.POWER_ON)
mock_next_boot.assert_called_once_with(task, self.info)
power_on_mock.assert_called_once_with(self.info)
@mock.patch.object(ipminative, '_power_off', autospec=True)
def test_set_power_off_ok(self, power_off_mock):
power_off_mock.return_value = states.POWER_OFF
@ -298,6 +312,40 @@ class IPMINativeDriverTestCase(db_base.DbTestCase):
# PXE is converted to 'network' internally by ipminative
ipmicmd.set_bootdev.assert_called_once_with('network', persist=False)
@mock.patch('pyghmi.ipmi.command.Command', autospec=True)
def test_force_set_boot_device_ok(self, ipmi_mock):
ipmicmd = ipmi_mock.return_value
ipmicmd.set_bootdev.return_value = None
with task_manager.acquire(self.context,
self.node.uuid) as task:
task.node.driver_info['ipmi_force_boot_device'] = True
self.driver.management.set_boot_device(task, boot_devices.PXE)
task.node.refresh()
self.assertEqual(
False,
task.node.driver_internal_info['is_next_boot_persistent']
)
# PXE is converted to 'network' internally by ipminative
ipmicmd.set_bootdev.assert_called_once_with('network', persist=False)
@mock.patch('pyghmi.ipmi.command.Command', autospec=True)
def test_set_boot_device_with_persistent(self, ipmi_mock):
ipmicmd = ipmi_mock.return_value
ipmicmd.set_bootdev.return_value = None
with task_manager.acquire(self.context,
self.node.uuid) as task:
task.node.driver_info['ipmi_force_boot_device'] = True
self.driver.management.set_boot_device(task,
boot_devices.PXE,
True)
self.assertEqual(
boot_devices.PXE,
task.node.driver_internal_info['persistent_boot_device'])
# PXE is converted to 'network' internally by ipminative
ipmicmd.set_bootdev.assert_called_once_with('network', persist=False)
def test_set_boot_device_bad_device(self):
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
@ -305,13 +353,15 @@ class IPMINativeDriverTestCase(db_base.DbTestCase):
task,
'fake-device')
@mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True)
@mock.patch.object(ipminative, '_reboot', autospec=True)
def test_reboot_ok(self, reboot_mock):
def test_reboot_ok(self, reboot_mock, mock_next_boot):
reboot_mock.return_value = None
with task_manager.acquire(self.context,
self.node.uuid) as task:
self.driver.power.reboot(task)
mock_next_boot.assert_called_once_with(task, self.info)
reboot_mock.assert_called_once_with(self.info)
@mock.patch('pyghmi.ipmi.command.Command', autospec=True)
@ -378,6 +428,14 @@ class IPMINativeDriverTestCase(db_base.DbTestCase):
self.assertEqual(expected,
self.driver.management.get_boot_device(task))
def test_get_force_boot_device_persistent(self):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.driver_info['ipmi_force_boot_device'] = True
task.node.driver_internal_info['persistent_boot_device'] = 'pxe'
bootdev = self.driver.management.get_boot_device(task)
self.assertEqual('pxe', bootdev['boot_device'])
self.assertTrue(bootdev['persistent'])
def test_management_interface_validate_good(self):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.management.validate(task)

View File

@ -39,6 +39,7 @@ from ironic.common import utils
from ironic.conductor import task_manager
from ironic.drivers.modules import console_utils
from ironic.drivers.modules import ipmitool as ipmi
from ironic.drivers import utils as driver_utils
from ironic.tests import base
from ironic.tests.conductor import utils as mgr_utils
from ironic.tests.db import base as db_base
@ -1205,6 +1206,23 @@ class IPMIToolDriverTestCase(db_base.DbTestCase):
mock_on.assert_called_once_with(self.info)
self.assertFalse(mock_off.called)
@mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True)
@mock.patch.object(ipmi, '_power_on', autospec=True)
@mock.patch.object(ipmi, '_power_off', autospec=True)
def test_set_power_on_with_next_boot(self, mock_off, mock_on,
mock_next_boot):
self.config(retry_timeout=0, group='ipmi')
mock_on.return_value = states.POWER_ON
with task_manager.acquire(self.context,
self.node['uuid']) as task:
self.driver.power.set_power_state(task,
states.POWER_ON)
mock_next_boot.assert_called_once_with(task, self.info)
mock_on.assert_called_once_with(self.info)
self.assertFalse(mock_off.called)
@mock.patch.object(ipmi, '_power_on', autospec=True)
@mock.patch.object(ipmi, '_power_off', autospec=True)
def test_set_power_off_ok(self, mock_off, mock_on):
@ -1296,9 +1314,10 @@ class IPMIToolDriverTestCase(db_base.DbTestCase):
self.driver.vendor.bmc_reset,
task, 'POST')
@mock.patch.object(driver_utils, 'ensure_next_boot_device', autospec=True)
@mock.patch.object(ipmi, '_power_off', spec_set=types.FunctionType)
@mock.patch.object(ipmi, '_power_on', spec_set=types.FunctionType)
def test_reboot_ok(self, mock_on, mock_off):
def test_reboot_ok(self, mock_on, mock_off, mock_next_boot):
manager = mock.MagicMock()
# NOTE(rloo): if autospec is True, then manager.mock_calls is empty
mock_on.return_value = states.POWER_ON
@ -1310,6 +1329,7 @@ class IPMIToolDriverTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context,
self.node['uuid']) as task:
self.driver.power.reboot(task)
mock_next_boot.assert_called_once_with(task, self.info)
self.assertEqual(manager.mock_calls, expected)
@ -1501,6 +1521,42 @@ class IPMIToolDriverTestCase(db_base.DbTestCase):
mock.call(self.info, "chassis bootdev pxe")]
mock_exec.assert_has_calls(mock_calls)
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
def test_management_interface_force_set_boot_device_ok(self, mock_exec):
mock_exec.return_value = [None, None]
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.driver_info['ipmi_force_boot_device'] = True
self.info['force_boot_device'] = True
self.driver.management.set_boot_device(task, boot_devices.PXE)
task.node.refresh()
self.assertEqual(
False,
task.node.driver_internal_info['is_next_boot_persistent']
)
mock_calls = [mock.call(self.info, "raw 0x00 0x08 0x03 0x08"),
mock.call(self.info, "chassis bootdev pxe")]
mock_exec.assert_has_calls(mock_calls)
@mock.patch.object(ipmi, '_exec_ipmitool', autospec=True)
def test_management_interface_set_boot_device_persistent(self, mock_exec):
mock_exec.return_value = [None, None]
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.driver_info['ipmi_force_boot_device'] = True
self.info['force_boot_device'] = True
self.driver.management.set_boot_device(task,
boot_devices.PXE,
True)
self.assertEqual(
boot_devices.PXE,
task.node.driver_internal_info['persistent_boot_device'])
mock_calls = [mock.call(self.info, "raw 0x00 0x08 0x03 0x08"),
mock.call(self.info, "chassis bootdev pxe")]
mock_exec.assert_has_calls(mock_calls)
def test_management_interface_set_boot_device_bad_device(self):
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
@ -1598,6 +1654,14 @@ class IPMIToolDriverTestCase(db_base.DbTestCase):
mock_exec.assert_called_with(mock.ANY,
"chassis bootparam get 5")
def test_get_force_boot_device_persistent(self):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.driver_info['ipmi_force_boot_device'] = True
task.node.driver_internal_info['persistent_boot_device'] = 'pxe'
bootdev = self.driver.management.get_boot_device(task)
self.assertEqual('pxe', bootdev['boot_device'])
self.assertTrue(bootdev['persistent'])
def test_management_interface_validate_good(self):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.management.validate(task)

View File

@ -18,6 +18,7 @@ import mock
from ironic.common import driver_factory
from ironic.common import exception
from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils
from ironic.drivers.modules import fake
from ironic.drivers import utils as driver_utils
from ironic.tests.conductor import utils as mgr_utils
@ -114,3 +115,48 @@ class UtilsTestCase(db_base.DbTestCase):
driver_utils.add_node_capability(task, 'a', 'b')
self.assertEqual('a:b,c:d,a:b',
task.node.properties['capabilities'])
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
def test_ensure_next_boot_device(self, node_set_boot_device_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.node.driver_internal_info['persistent_boot_device'] = 'pxe'
driver_utils.ensure_next_boot_device(
task,
{'force_boot_device': True}
)
node_set_boot_device_mock.assert_called_once_with(task, 'pxe')
def test_ensure_next_boot_device_clears_is_next_boot_persistent(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.node.driver_internal_info['persistent_boot_device'] = 'pxe'
task.node.driver_internal_info['is_next_boot_persistent'] = False
driver_utils.ensure_next_boot_device(
task,
{'force_boot_device': True}
)
task.node.refresh()
self.assertNotIn('is_next_boot_persistent',
task.node.driver_internal_info)
def test_force_persistent_boot_true(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.node.driver_info['ipmi_force_boot_device'] = True
ret = driver_utils.force_persistent_boot(task, 'pxe', True)
self.assertEqual(None, ret)
task.node.refresh()
self.assertIn('persistent_boot_device',
task.node.driver_internal_info)
def test_force_persistent_boot_false(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
ret = driver_utils.force_persistent_boot(task, 'pxe', False)
self.assertEqual(None, ret)
task.node.refresh()
self.assertEqual(
False,
task.node.driver_internal_info.get('is_next_boot_persistent')
)