Add support for local boot

This patch implements the part that reads the specified boot_option in
the node.instance_info['capabilities'] that have been passed to Ironic by
the Nova Ironic Driver and pass that information to the deploy ramdisk
by adding it to the kernel cmdline in the PXE configuration file that
will be generated to that node.

This patch also makes sure that we clean the PXE configuration files for
the node marked to local boot after it's deployed, so that any attempt
to boot it from the network will not work.

This patch only apply to the pxe_* drivers, because the blueprint is
about adding local boot support for deployments that uses partition
images. The agent driver right now supports full disk images only.

Implements: blueprint local-boot-support-with-partition-images
Change-Id: Ide08e2b41dcf74c69dfbce242112da701fa15187
This commit is contained in:
Lucas Alvares Gomes 2015-01-09 19:16:24 +00:00
parent c14e3dc46e
commit a4cf7149fb
12 changed files with 294 additions and 43 deletions

View File

@ -29,6 +29,7 @@ from oslo.utils import excutils
from oslo.utils import units
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_serialization import jsonutils
import requests
import six
@ -677,3 +678,39 @@ def get_single_nic_with_vif_port_id(task):
for port in task.ports:
if port.extra.get('vif_port_id'):
return port.address
def parse_instance_info_capabilities(node):
"""Parse the instance_info capabilities.
These capabilities are defined in the Flavor extra_spec and passed
to Ironic by the Nova Ironic driver.
NOTE: Although our API fully supports JSON fields, to maintain the
backward compatibility with Juno the Nova Ironic driver is sending
it as a string.
:param node: a single Node.
:raises: InvalidParameterValue if the capabilities string is not a
dictionary or is malformed.
:returns: A dictionary with the capabilities if found, otherwise an
empty dictionary.
"""
def parse_error():
error_msg = (_("Error parsing capabilities from Node %s instance_info "
"field. A dictionary or a dictionary string is "
"expected.") % node.uuid)
raise exception.InvalidParameterValue(error_msg)
capabilities = node.instance_info.get('capabilities', {})
if isinstance(capabilities, six.string_types):
try:
capabilities = jsonutils.loads(capabilities)
except (ValueError, TypeError):
parse_error()
if not isinstance(capabilities, dict):
parse_error()
return capabilities

View File

@ -5,7 +5,7 @@ dhcp
goto deploy
:deploy
kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %}
kernel {{ pxe_options.deployment_aki_path }} selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} boot_option={{ pxe_options.boot_option }} ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %}
initrd {{ pxe_options.deployment_ari_path }}
boot

View File

@ -316,6 +316,18 @@ def parse_root_device_hints(node):
return ','.join(hints)
def get_boot_option(node):
"""Get the boot mode.
:param node: A single Node.
:raises: InvalidParameterValue if the capabilities string is not a
dict or is malformed.
:returns: A string representing the boot mode type. Defaults to 'netboot'.
"""
capabilities = deploy_utils.parse_instance_info_capabilities(node)
return capabilities.get('boot_option', 'netboot').lower()
def build_deploy_ramdisk_options(node):
"""Build the ramdisk config options for a node
@ -343,6 +355,7 @@ def build_deploy_ramdisk_options(node):
'iscsi_target_iqn': "iqn-%s" % node.uuid,
'ironic_api_url': ironic_api,
'disk': CONF.pxe.disk_devices,
'boot_option': get_boot_option(node),
}
root_device = parse_root_device_hints(node)

View File

@ -22,6 +22,7 @@ import shutil
from oslo_config import cfg
from ironic.common import boot_devices
from ironic.common import dhcp_factory
from ironic.common import exception
from ironic.common.i18n import _
@ -267,6 +268,24 @@ def _destroy_token_file(node):
utils.unlink_without_raise(token_file_path)
def try_set_boot_device(task, device, persistent=True):
# NOTE(faizan): Under UEFI boot mode, setting of boot device may differ
# between different machines. IPMI does not work for setting boot
# devices in UEFI mode for certain machines.
# Expected IPMI failure for uefi boot mode. Logging a message to
# set the boot device manually and continue with deploy.
try:
manager_utils.node_set_boot_device(task, device, persistent=persistent)
except exception.IPMIFailure:
if driver_utils.get_node_capability(task.node,
'boot_mode') == 'uefi':
LOG.warning(_LW("ipmitool is unable to set boot device while "
"the node %s is in UEFI boot mode. Please set "
"the boot device manually.") % task.node.uuid)
else:
raise
class PXEDeploy(base.DeployInterface):
"""PXE Deploy Interface for deploy-related actions."""
@ -280,9 +299,24 @@ class PXEDeploy(base.DeployInterface):
:raises: InvalidParameterValue.
:raises: MissingParameterValue
"""
node = task.node
# Check the boot_mode capability parameter value.
driver_utils.validate_boot_mode_capability(task.node)
# Check the boot_mode and boot_option capabilities values.
driver_utils.validate_boot_mode_capability(node)
driver_utils.validate_boot_option_capability(node)
boot_mode = driver_utils.get_node_capability(node, 'boot_mode')
boot_option = driver_utils.get_node_capability(node, 'boot_option')
# NOTE(lucasagomes): We don't support UEFI + localboot because
# we do not support creating an EFI boot partition, including the
# EFI modules and managing the bootloader variables via efibootmgr.
if boot_mode == 'uefi' and boot_option == 'local':
error_msg = (_("Local boot is requested, but can't be "
"used with node %s because it's configured "
"to use UEFI boot") % node.uuid)
LOG.error(error_msg)
raise exception.InvalidParameterValue(error_msg)
if CONF.pxe.ipxe_enabled:
if not CONF.pxe.http_url or not CONF.pxe.http_root:
@ -290,16 +324,15 @@ class PXEDeploy(base.DeployInterface):
"iPXE boot is enabled but no HTTP URL or HTTP "
"root was specified."))
# iPXE and UEFI should not be configured together.
if driver_utils.get_node_capability(task.node,
'boot_mode') == 'uefi':
if boot_mode == 'uefi':
LOG.error(_LE("UEFI boot mode is not supported with "
"iPXE boot enabled."))
raise exception.InvalidParameterValue(_(
"Conflict: iPXE is enabled, but cannot be used with node"
"%(node_uuid)s configured to use UEFI boot") %
{'node_uuid': task.node.uuid})
{'node_uuid': node.uuid})
d_info = _parse_deploy_info(task.node)
d_info = _parse_deploy_info(node)
iscsi_deploy.validate(task)
@ -331,22 +364,7 @@ class PXEDeploy(base.DeployInterface):
provider = dhcp_factory.DHCPFactory()
provider.update_dhcp(task, dhcp_opts)
# NOTE(faizan): Under UEFI boot mode, setting of boot device may differ
# between different machines. IPMI does not work for setting boot
# devices in UEFI mode for certain machines.
# Expected IPMI failure for uefi boot mode. Logging a message to
# set the boot device manually and continue with deploy.
try:
manager_utils.node_set_boot_device(task, 'pxe', persistent=True)
except exception.IPMIFailure:
if driver_utils.get_node_capability(task.node,
'boot_mode') == 'uefi':
LOG.warning(_LW("ipmitool is unable to set boot device while "
"the node is in UEFI boot mode."
"Please set the boot device manually."))
else:
raise
try_set_boot_device(task, boot_devices.PXE)
manager_utils.node_power_action(task, states.REBOOT)
return states.DEPLOYWAIT
@ -390,6 +408,9 @@ class PXEDeploy(base.DeployInterface):
pxe_utils.create_pxe_config(task, pxe_options,
pxe_config_template)
# FIXME(lucasagomes): If it's local boot we should not cache
# the image kernel and ramdisk (Or even require it).
_cache_ramdisk_kernel(task.context, task.node, pxe_info)
def clean_up(self, task):
@ -425,6 +446,13 @@ class PXEDeploy(base.DeployInterface):
provider = dhcp_factory.DHCPFactory()
provider.update_dhcp(task, dhcp_opts)
if iscsi_deploy.get_boot_option(task.node) == "local":
# If it's going to boot from the local disk, we don't need
# PXE config files. They still need to be generated as part
# of the prepare() because the deployment does PXE boot the
# deploy ramdisk
pxe_utils.clean_up_pxe_config(task)
class VendorPassthru(base.VendorInterface):
"""Interface to mix IPMI and PXE vendor-specific interfaces."""
@ -444,6 +472,7 @@ class VendorPassthru(base.VendorInterface):
:param kwargs: kwargs containins the method's parameters.
:raises: InvalidParameterValue if any parameters is invalid.
"""
driver_utils.validate_boot_option_capability(task.node)
iscsi_deploy.get_deploy_info(task.node, **kwargs)
@base.passthru(['POST'], method='pass_deploy_info')
@ -469,12 +498,17 @@ class VendorPassthru(base.VendorInterface):
return
try:
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid)
deploy_utils.switch_pxe_config(pxe_config_path, root_uuid,
driver_utils.get_node_capability(node, 'boot_mode'))
if iscsi_deploy.get_boot_option(node) == "local":
try_set_boot_device(task, boot_devices.DISK)
# If it's going to boot from the local disk, get rid of
# the PXE configuration files used for the deployment
pxe_utils.clean_up_pxe_config(task)
else:
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid)
deploy_utils.switch_pxe_config(pxe_config_path, root_uuid,
driver_utils.get_node_capability(node, 'boot_mode'))
deploy_utils.notify_deploy_complete(kwargs['address'])
LOG.info(_LI('Deployment to node %s done'), node.uuid)
task.process_event('done')
except Exception as e:

View File

@ -2,7 +2,7 @@ default deploy
label deploy
kernel {{ pxe_options.deployment_aki_path }}
append initrd={{ pxe_options.deployment_ari_path }} rootfstype=ramfs selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %}
append initrd={{ pxe_options.deployment_ari_path }} rootfstype=ramfs selinux=0 disk={{ pxe_options.disk }} iscsi_target_iqn={{ pxe_options.iscsi_target_iqn }} deployment_id={{ pxe_options.deployment_id }} deployment_key={{ pxe_options.deployment_key }} ironic_api_url={{ pxe_options.ironic_api_url }} troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} boot_option={{ pxe_options.boot_option }} {% if pxe_options.root_device %}root_device={{ pxe_options.root_device }}{% endif %}
ipappend 3

View File

@ -198,16 +198,44 @@ def add_node_capability(task, capability, value):
node.save()
def validate_capability(node, capability_name, valid_values):
"""Validate a capabability set in node property
:param node: an ironic node object.
:param capability_name: the name of the capability.
:parameter valid_values: an iterable with valid values expected for
that capability.
:raises: InvalidParameterValue, if the capability is not set to the
expected values.
"""
value = get_node_capability(node, capability_name)
if value and value not in valid_values:
valid_value_str = ', '.join(valid_values)
raise exception.InvalidParameterValue(
_("Invalid %(capability)s parameter '%(value)s'. "
"Acceptable values are: %(valid_values)s.") %
{'capability': capability_name, 'value': value,
'valid_values': valid_value_str})
def validate_boot_mode_capability(node):
"""Validate the boot_mode capability set in node property.
"""Validate the boot_mode capability set in node properties.
:param node: an ironic node object.
:raises: InvalidParameterValue, if 'boot_mode' capability is set
other than 'bios' or 'uefi' or None.
"""
boot_mode = get_node_capability(node, 'boot_mode')
validate_capability(node, 'boot_mode', ('bios', 'uefi'))
if boot_mode and boot_mode not in ['bios', 'uefi']:
raise exception.InvalidParameterValue(_("Invalid boot_mode "
"parameter '%s'.") % boot_mode)
def validate_boot_option_capability(node):
"""Validate the boot_option capability set in node properties.
:param node: an ironic node object.
:raises: InvalidParameterValue, if 'boot_option' capability is set
other than 'local' or 'netboot' or None.
"""
validate_capability(node, 'boot_option', ('local', 'netboot'))

View File

@ -2,7 +2,7 @@ default deploy
label deploy
kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_kernel
append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_ramdisk rootfstype=ramfs selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 troubleshoot=0 text test_param root_device=vendor=fake,size=123
append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/deploy_ramdisk rootfstype=ramfs selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=iqn-1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_id=1be26c0b-03f2-4d2e-ae87-c02d7f33c123 deployment_key=0123456789ABCDEFGHIJKLMNOPQRSTUV ironic_api_url=http://192.168.122.184:6385 troubleshoot=0 text test_param boot_option=netboot root_device=vendor=fake,size=123
ipappend 3

View File

@ -1081,3 +1081,27 @@ class VirtualMediaDeployUtilsTestCase(db_base.DbTestCase):
shared=False) as task:
address = utils.get_single_nic_with_vif_port_id(task)
self.assertEqual('aa:bb:cc', address)
class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
def setUp(self):
super(ParseInstanceInfoCapabilitiesTestCase, self).setUp()
self.node = obj_utils.get_test_node(self.context, driver='fake')
def test_parse_instance_info_capabilities_string(self):
self.node.instance_info = {'capabilities': '{"cat": "meow"}'}
expected_result = {"cat": "meow"}
result = utils.parse_instance_info_capabilities(self.node)
self.assertEqual(expected_result, result)
def test_parse_instance_info_capabilities(self):
self.node.instance_info = {'capabilities': {"dog": "wuff"}}
expected_result = {"dog": "wuff"}
result = utils.parse_instance_info_capabilities(self.node)
self.assertEqual(expected_result, result)
def test_parse_instance_info_invalid_type(self):
self.node.instance_info = {'capabilities': 'not-a-dict'}
self.assertRaises(exception.InvalidParameterValue,
utils.parse_instance_info_capabilities, self.node)

View File

@ -214,7 +214,8 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase):
mock_rmtree.assert_called_once_with('/path/uuid')
def _test_build_deploy_ramdisk_options(self, mock_alnum, api_url,
expected_root_device=None):
expected_root_device=None,
expected_boot_option='netboot'):
fake_key = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
fake_disk = 'fake-disk'
@ -226,7 +227,8 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase):
'deployment_id': self.node.uuid,
'deployment_key': fake_key,
'disk': fake_disk,
'ironic_api_url': api_url}
'ironic_api_url': api_url,
'boot_option': expected_boot_option}
if expected_root_device:
expected_opts['root_device'] = expected_root_device
@ -272,6 +274,17 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase):
self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url,
expected_root_device=expected)
@mock.patch.object(keystone, 'get_service_url')
@mock.patch.object(utils, 'random_alnum')
def test_build_deploy_ramdisk_options_boot_option(self, mock_alnum,
mock_get_url):
self.node.instance_info = {'capabilities': '{"boot_option": "local"}'}
expected = 'local'
fake_api_url = 'http://127.0.0.1:6385'
self.config(api_url=fake_api_url, group='conductor')
self._test_build_deploy_ramdisk_options(mock_alnum, fake_api_url,
expected_boot_option=expected)
def test_parse_root_device_hints(self):
self.node.properties['root_device'] = {'wwn': 123456}
expected = 'wwn=123456'
@ -288,3 +301,13 @@ class IscsiDeployMethodsTestCase(db_base.DbTestCase):
self.node.properties = {}
result = iscsi_deploy.parse_root_device_hints(self.node)
self.assertIsNone(result)
def test_get_boot_option(self):
self.node.instance_info = {'capabilities': '{"boot_option": "local"}'}
result = iscsi_deploy.get_boot_option(self.node)
self.assertEqual("local", result)
def test_get_boot_option_default_value(self):
self.node.instance_info = {}
result = iscsi_deploy.get_boot_option(self.node)
self.assertEqual("netboot", result)

View File

@ -25,6 +25,7 @@ import mock
from oslo_config import cfg
from oslo_serialization import jsonutils as json
from ironic.common import boot_devices
from ironic.common import dhcp_factory
from ironic.common import exception
from ironic.common.glance_service import base_image_service
@ -158,7 +159,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
'deployment_id': 'fake-deploy-id',
'deployment_key': 'fake-deploy-key',
'disk': 'fake-disk',
'ironic_api_url': 'fake-api-url'}
'ironic_api_url': 'fake-api-url',
'boot_option': 'netboot'}
deploy_opts_mock.return_value = fake_deploy_opts
@ -193,7 +195,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
'pxe_append_params': 'test_param',
'aki_path': kernel,
'deployment_aki_path': deploy_kernel,
'tftp_server': tftp_server
'tftp_server': tftp_server,
'boot_option': 'netboot'
}
expected_options.update(fake_deploy_opts)
@ -353,6 +356,28 @@ class PXEDriverTestCase(db_base.DbTestCase):
self.assertRaises(exception.InvalidParameterValue,
task.driver.deploy.validate, task)
@mock.patch.object(base_image_service.BaseImageService, '_show')
def test_validate_fail_invalid_boot_option(self, mock_glance):
properties = {'capabilities': 'boot_option:foo,dog:wuff'}
mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel',
'ramdisk_id': 'fake-initr'}}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
task.driver.deploy.validate, task)
@mock.patch.object(base_image_service.BaseImageService, '_show')
def test_validate_fail_invalid_uefi_and_localboot(self, mock_glance):
properties = {'capabilities': 'boot_mode:uefi,boot_option:local'}
mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel',
'ramdisk_id': 'fake-initr'}}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
task.driver.deploy.validate, task)
def test_validate_fail_no_port(self):
new_node = obj_utils.create_test_node(
self.context,
@ -580,12 +605,34 @@ class PXEDriverTestCase(db_base.DbTestCase):
update_dhcp_mock.assert_called_once_with(
task, dhcp_opts)
@mock.patch.object(pxe_utils, 'clean_up_pxe_config')
@mock.patch.object(dhcp_factory.DHCPFactory, 'update_dhcp')
def test_take_over_localboot(self, update_dhcp_mock, clean_pxe_mock):
with task_manager.acquire(
self.context, self.node.uuid, shared=True) as task:
task.node.instance_info['capabilities'] = {"boot_option": "local"}
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
task.driver.deploy.take_over(task)
update_dhcp_mock.assert_called_once_with(
task, dhcp_opts)
clean_pxe_mock.assert_called_once_with(task)
@mock.patch.object(pxe_utils, 'clean_up_pxe_config')
@mock.patch.object(manager_utils, 'node_set_boot_device')
@mock.patch.object(deploy_utils, 'notify_deploy_complete')
@mock.patch.object(deploy_utils, 'switch_pxe_config')
@mock.patch.object(iscsi_deploy, 'InstanceImageCache')
def test_continue_deploy_good(self, mock_image_cache, mock_switch_config,
notify_mock):
def _test_continue_deploy(self, is_localboot, mock_image_cache,
mock_switch_config, notify_mock,
mock_node_boot_dev, mock_clean_pxe):
token_path = self._create_token_file()
# set local boot
if is_localboot:
i_info = self.node.instance_info
i_info['capabilities'] = '{"boot_option": "local"}'
self.node.instance_info = i_info
self.node.power_state = states.POWER_ON
self.node.provision_state = states.DEPLOYWAIT
self.node.target_provision_state = states.ACTIVE
@ -614,9 +661,23 @@ class PXEDriverTestCase(db_base.DbTestCase):
mock_image_cache.assert_called_once_with()
mock_image_cache.return_value.clean_up.assert_called_once_with()
pxe_config_path = pxe_utils.get_pxe_config_file_path(self.node.uuid)
mock_switch_config.assert_called_once_with(pxe_config_path, root_uuid,
boot_mode)
notify_mock.assert_called_once_with('123456')
if is_localboot:
mock_node_boot_dev.assert_called_once_with(
mock.ANY, boot_devices.DISK, persistent=True)
mock_clean_pxe.assert_called_once_with(mock.ANY)
self.assertFalse(mock_switch_config.called)
else:
mock_switch_config.assert_called_once_with(
pxe_config_path, root_uuid, boot_mode)
self.assertFalse(mock_node_boot_dev.called)
self.assertFalse(mock_clean_pxe.called)
def test_continue_deploy(self):
self._test_continue_deploy(False)
def test_continue_deploy_localboot(self):
self._test_continue_deploy(True)
@mock.patch.object(iscsi_deploy, 'InstanceImageCache')
def test_continue_deploy_fail(self, mock_image_cache):

View File

@ -133,6 +133,22 @@ class UtilsTestCase(db_base.DbTestCase):
self.assertIsNone(driver_utils.rm_node_capability(task, 'x'))
self.assertEqual('a:b', task.node.properties['capabilities'])
def test_validate_capability(self):
properties = {'capabilities': 'cat:meow,cap2:value2'}
self.node.properties = properties
result = driver_utils.validate_capability(
self.node, 'cat', ['meow', 'purr'])
self.assertIsNone(result)
def test_validate_capability_with_exception(self):
properties = {'capabilities': 'cat:bark,cap2:value2'}
self.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
driver_utils.validate_capability,
self.node, 'cat', ['meow', 'purr'])
def test_validate_boot_mode_capability(self):
properties = {'capabilities': 'boot_mode:uefi,cap2:value2'}
self.node.properties = properties
@ -146,3 +162,17 @@ class UtilsTestCase(db_base.DbTestCase):
self.assertRaises(exception.InvalidParameterValue,
driver_utils.validate_boot_mode_capability, self.node)
def test_validate_boot_option_capability(self):
properties = {'capabilities': 'boot_option:netboot,cap2:value2'}
self.node.properties = properties
result = driver_utils.validate_boot_option_capability(self.node)
self.assertIsNone(result)
def test_validate_boot_option_capability_with_exception(self):
properties = {'capabilities': 'boot_option:foo,cap2:value2'}
self.node.properties = properties
self.assertRaises(exception.InvalidParameterValue,
driver_utils.validate_boot_option_capability, self.node)

View File

@ -49,7 +49,8 @@ class TestPXEUtils(db_base.DbTestCase):
'deployment_aki_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-'
u'c02d7f33c123/deploy_kernel',
'disk': 'cciss/c0d0,sda,hda,vda',
'root_device': 'vendor=fake,size=123'
'root_device': 'vendor=fake,size=123',
'boot_option': 'netboot',
}
self.agent_pxe_options = {
'deployment_ari_path': u'/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7'