From 6883674b3c4b016ba36b73bb385ec8813d88f93f Mon Sep 17 00:00:00 2001 From: Joanna Taryma Date: Mon, 10 Apr 2017 16:01:44 -0700 Subject: [PATCH] iPXE template support for iSCSI Added support for iPXE template output of a template containing iscsi based URL sanhook entries to enable boot as well as additional attachments. Authored-By: Julia Kreger Co-Authored-By: Joanna Taryma Co-Authored-By: Michael Turek Change-Id: I75869262dbfd1caa779fa21e93cdb31f193cb829 Partial-Bug: #1559691 --- ironic/drivers/modules/deploy_utils.py | 26 +++- ironic/drivers/modules/ipxe_config.template | 20 +++ ironic/drivers/modules/pxe.py | 68 ++++++++- ironic/tests/unit/common/test_pxe_utils.py | 51 +++++++ .../ipxe_config_boot_from_volume.template | 33 +++++ ...onfig_boot_from_volume_no_volumes.template | 32 +++++ .../unit/drivers/modules/test_deploy_utils.py | 69 +++++++++ ironic/tests/unit/drivers/modules/test_pxe.py | 133 +++++++++++++++++- 8 files changed, 424 insertions(+), 8 deletions(-) create mode 100644 ironic/tests/unit/drivers/ipxe_config_boot_from_volume.template create mode 100644 ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_volumes.template diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 2007e82dbe..ffdb53bd29 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -267,11 +267,13 @@ def _replace_root_uuid(path, root_uuid): def _replace_boot_line(path, boot_mode, is_whole_disk_image, - trusted_boot=False): + trusted_boot=False, iscsi_boot=False): if is_whole_disk_image: boot_disk_type = 'boot_whole_disk' elif trusted_boot: boot_disk_type = 'trusted_boot' + elif iscsi_boot: + boot_disk_type = 'boot_iscsi' else: boot_disk_type = 'boot_partition' @@ -292,7 +294,8 @@ def _replace_disk_identifier(path, disk_identifier): def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, - is_whole_disk_image, trusted_boot=False): + is_whole_disk_image, trusted_boot=False, + iscsi_boot=False): """Switch a pxe config from deployment mode to service mode. :param path: path to the pxe config file in tftpboot. @@ -303,13 +306,15 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, :param trusted_boot: if boot with trusted_boot or not. The usage of is_whole_disk_image and trusted_boot are mutually exclusive. You can have one or neither, but not both. + :param iscsi_boot: if boot is from an iSCSI volume or not. """ if not is_whole_disk_image: _replace_root_uuid(path, root_uuid_or_disk_id) else: _replace_disk_identifier(path, root_uuid_or_disk_id) - _replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot) + _replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot, + iscsi_boot) def get_dev(address, port, iqn, lun): @@ -1297,3 +1302,18 @@ def tear_down_storage_configuration(task): driver_internal_info.pop('boot_from_volume_deploy', None) node.driver_internal_info = driver_internal_info node.save() + + +def is_iscsi_boot(task): + """Return true if booting from an iscsi volume.""" + node = task.node + volume = node.driver_internal_info.get('boot_from_volume') + if volume: + try: + boot_volume = objects.VolumeTarget.get_by_uuid( + task.context, volume) + if boot_volume.volume_type == 'iscsi': + return True + except exception.VolumeTargetNotFound: + return False + return False diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template index b4dd73465c..5b6be48a2a 100644 --- a/ironic/drivers/modules/ipxe_config.template +++ b/ironic/drivers/modules/ipxe_config.template @@ -14,6 +14,26 @@ imgfree kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.aki_path }} root={{ ROOT }} ro text {{ pxe_options.pxe_append_params|default("", true) }} initrd=ramdisk || goto boot_partition initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_partition boot +{%- if pxe_options.boot_from_volume %} + +:boot_iscsi +imgfree +{% if pxe_options.username %}set username {{ pxe_options.username }}{% endif %} +{% if pxe_options.password %}set password {{ pxe_options.password }}{% endif %} +{% if pxe_options.iscsi_initiator_iqn %}set initiator-iqn {{ pxe_options.iscsi_initiator_iqn }}{% endif %} +sanhook --drive 0x80 {{ pxe_options.iscsi_boot_url }} || goto fail_iscsi_retry +{%- if pxe_options.iscsi_volumes %}{% for volume in pxe_options +.iscsi_volumes %} +{%- set drive_id = 80 + loop.index %} +sanhook --drive 0x{{ drive_id }} {{ volume }} || goto fail_iscsi_retry +{%- endfor %}{% endif %} +sanboot --no-describe || goto fail_iscsi_retry + +:fail_iscsi_retry +echo Failed to attach iSCSI volume(s), retrying in 10 seconds. +sleep 10 +goto boot_iscsi +{%- endif %} :boot_whole_disk sanboot --no-describe diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 4524352dd2..435e6ee763 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -37,7 +37,7 @@ from ironic.drivers import base from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import image_cache from ironic.drivers import utils as driver_utils - +from ironic import objects LOG = logging.getLogger(__name__) METRICS = metrics_utils.get_metrics_logger(__name__) @@ -212,6 +212,9 @@ def _build_pxe_config_options(task, pxe_info, service=False): """ if service: pxe_options = {} + elif (task.node.driver_internal_info.get('boot_from_volume') and + CONF.pxe.ipxe_enabled): + pxe_options = _get_volume_pxe_options(task) else: pxe_options = _build_deploy_pxe_options(task, pxe_info) @@ -243,7 +246,65 @@ def _build_service_pxe_config(task, instance_image_info, deploy_utils.switch_pxe_config( pxe_config_path, root_uuid_or_disk_id, deploy_utils.get_boot_mode_for_deploy(node), - iwdi, deploy_utils.is_trusted_boot_requested(node)) + iwdi, deploy_utils.is_trusted_boot_requested(node), + deploy_utils.is_iscsi_boot(task)) + + +def _get_volume_pxe_options(task): + """Identify volume information for iPXE template generation.""" + def __return_item_or_first_if_list(item): + if isinstance(item, list): + return item[0] + else: + return item + + def __get_property(properties, key): + prop = __return_item_or_first_if_list(properties.get(key, '')) + if prop is not '': + return prop + return __return_item_or_first_if_list(properties.get(key + 's', '')) + + def __generate_iscsi_url(properties): + """Returns iscsi url.""" + portal = __get_property(properties, 'target_portal') + iqn = __get_property(properties, 'target_iqn') + lun = __get_property(properties, 'target_lun') + + if ':' in portal: + host, port = portal.split(':') + else: + host = portal + port = '' + return ("iscsi:%(host)s::%(port)s:%(lun)s:%(iqn)s" % + {'host': host, 'port': port, 'lun': lun, 'iqn': iqn}) + + pxe_options = {} + node = task.node + boot_volume = node.driver_internal_info.get('boot_from_volume') + volume = objects.VolumeTarget.get_by_uuid(task.context, + boot_volume) + properties = volume.properties + if 'iscsi' in volume['volume_type']: + if 'auth_username' in properties: + pxe_options['username'] = properties['auth_username'] + if 'auth_password' in properties: + pxe_options['password'] = properties['auth_password'] + pxe_options.update( + {'iscsi_boot_url': __generate_iscsi_url(volume.properties), + 'iscsi_initiator_iqn': (__get_property(properties, + 'target_iqn') or None)}) + # NOTE(TheJulia): This may be the route to multi-path, define + # volumes via sanhook in the ipxe template and let the OS sort it out. + additional_targets = [] + for target in task.volume_targets: + if target.boot_index != 0 and 'iscsi' in target.volume_type: + additional_targets.append( + __generate_iscsi_url(target.properties)) + pxe_options.update({'iscsi_volumes': additional_targets, + 'boot_from_volume': True}) + # TODO(TheJulia): FibreChannel boot, i.e. wwpn in volume_type + # for FCoE, should go here. + return pxe_options @METRICS.timer('validate_boot_option_for_trusted_boot') @@ -312,6 +373,9 @@ def _clean_up_pxe_env(task, images_info): class PXEBoot(base.BootInterface): + def __init__(self): + self.capabilities = ['iscsi_volume_boot'] + def get_properties(self): """Return the properties of the interface. diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index c4e5373424..2e9f0cbf72 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -64,6 +64,16 @@ class TestPXEUtils(db_base.DbTestCase): 'ipxe_timeout': 120 }) + self.ipxe_options_boot_from_volume = self.ipxe_options.copy() + self.ipxe_options_boot_from_volume.update({ + 'boot_from_volume': True, + 'iscsi_boot_url': 'iscsi:fake_host::3260:0:fake_iqn', + 'iscsi_initiator_iqn': 'fake_iqn', + 'iscsi_volumes': ['iscsi:fake_host::3260:1:fake_iqn'], + 'username': 'fake_username', + 'password': 'fake_password' + }) + self.node = object_utils.create_test_node(self.context) def test_default_pxe_config(self): @@ -133,6 +143,47 @@ class TestPXEUtils(db_base.DbTestCase): self.assertEqual(six.text_type(expected_template), rendered_template) + def test_default_ipxe_boot_from_volume_config(self): + self.config( + pxe_config_template='ironic/drivers/modules/ipxe_config.template', + group='pxe' + ) + self.config(http_url='http://1.2.3.4:1234', group='deploy') + rendered_template = utils.render_template( + CONF.pxe.pxe_config_template, + {'pxe_options': self.ipxe_options_boot_from_volume, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) + + templ_file = 'ironic/tests/unit/drivers/' \ + 'ipxe_config_boot_from_volume.template' + with open(templ_file) as f: + expected_template = f.read().rstrip() + + self.assertEqual(six.text_type(expected_template), rendered_template) + + def test_default_ipxe_boot_from_volume_config_no_volumes(self): + self.config( + pxe_config_template='ironic/drivers/modules/ipxe_config.template', + group='pxe' + ) + self.config(http_url='http://1.2.3.4:1234', group='deploy') + + pxe_options = self.ipxe_options_boot_from_volume + pxe_options['iscsi_volumes'] = [] + + rendered_template = utils.render_template( + CONF.pxe.pxe_config_template, + {'pxe_options': pxe_options, + 'ROOT': '{{ ROOT }}', + 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) + + templ_file = 'ironic/tests/unit/drivers/' \ + 'ipxe_config_boot_from_volume_no_volumes.template' + with open(templ_file) as f: + expected_template = f.read().rstrip() + self.assertEqual(six.text_type(expected_template), rendered_template) + # NOTE(TheJulia): Remove elilo support after the deprecation period, # in the Queens release. def test_default_elilo_config(self): diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume.template new file mode 100644 index 0000000000..480d4637a1 --- /dev/null +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume.template @@ -0,0 +1,33 @@ +#!ipxe + +goto deploy + +:deploy +imgfree +kernel http://1.2.3.4:1234/deploy_kernel selinux=0 troubleshoot=0 text test_param ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} ipa-api-url=http://192.168.122.184:6385 initrd=deploy_ramdisk coreos.configdrive=0 || goto deploy + +initrd http://1.2.3.4:1234/deploy_ramdisk || goto deploy +boot + +:boot_partition +imgfree +kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramdisk || goto boot_partition +initrd http://1.2.3.4:1234/ramdisk || goto boot_partition +boot + +:boot_iscsi +imgfree +set username fake_username +set password fake_password +set initiator-iqn fake_iqn +sanhook --drive 0x80 iscsi:fake_host::3260:0:fake_iqn || goto fail_iscsi_retry +sanhook --drive 0x81 iscsi:fake_host::3260:1:fake_iqn || goto fail_iscsi_retry +sanboot --no-describe || goto fail_iscsi_retry + +:fail_iscsi_retry +echo Failed to attach iSCSI volume(s), retrying in 10 seconds. +sleep 10 +goto boot_iscsi + +:boot_whole_disk +sanboot --no-describe \ No newline at end of file diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_volumes.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_volumes.template new file mode 100644 index 0000000000..3229eca22a --- /dev/null +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_volumes.template @@ -0,0 +1,32 @@ +#!ipxe + +goto deploy + +:deploy +imgfree +kernel http://1.2.3.4:1234/deploy_kernel selinux=0 troubleshoot=0 text test_param ip=${ip}:${next-server}:${gateway}:${netmask} BOOTIF=${mac} ipa-api-url=http://192.168.122.184:6385 initrd=deploy_ramdisk coreos.configdrive=0 || goto deploy + +initrd http://1.2.3.4:1234/deploy_ramdisk || goto deploy +boot + +:boot_partition +imgfree +kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramdisk || goto boot_partition +initrd http://1.2.3.4:1234/ramdisk || goto boot_partition +boot + +:boot_iscsi +imgfree +set username fake_username +set password fake_password +set initiator-iqn fake_iqn +sanhook --drive 0x80 iscsi:fake_host::3260:0:fake_iqn || goto fail_iscsi_retry +sanboot --no-describe || goto fail_iscsi_retry + +:fail_iscsi_retry +echo Failed to attach iSCSI volume(s), retrying in 10 seconds. +sleep 10 +goto boot_iscsi + +:boot_whole_disk +sanboot --no-describe \ No newline at end of file diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index 342985c2b5..103e952503 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -206,6 +206,29 @@ append mbr:0x12345678 boot """ +_IPXECONF_BOOT_ISCSI_NO_CONFIG = """ +#!ipxe + +dhcp + +goto boot_iscsi + +:deploy +kernel deploy_kernel +initrd deploy_ramdisk +boot + +:boot_partition +kernel kernel +append initrd=ramdisk root=UUID=0x12345678 +boot + +:boot_whole_disk +kernel chain.c32 +append mbr:{{ DISK_IDENTIFIER }} +boot +""" + _UEFI_PXECONF_DEPLOY = b""" default=deploy @@ -936,6 +959,18 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): pxeconf = f.read() self.assertEqual(_IPXECONF_BOOT_WHOLE_DISK, pxeconf) + def test_switch_ipxe_iscsi_boot(self): + boot_mode = 'iscsi' + cfg.CONF.set_override('ipxe_enabled', True, 'pxe') + fname = self._create_config(boot_mode=boot_mode, ipxe=True) + utils.switch_pxe_config(fname, + '0x12345678', + boot_mode, + False, False, True) + with open(fname, 'r') as f: + pxeconf = f.read() + self.assertEqual(_IPXECONF_BOOT_ISCSI_NO_CONFIG, pxeconf) + class GetPxeBootConfigTestCase(db_base.DbTestCase): @@ -2500,3 +2535,37 @@ class TestStorageInterfaceUtils(db_base.DbTestCase): with task_manager.acquire( self.context, self.node.uuid, shared=False) as task: self.assertEqual(0, len(task.volume_targets)) + + def test_is_iscsi_boot(self): + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id) + self.node.driver_internal_info = {'boot_from_volume': vol_id} + self.node.save() + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + self.assertTrue(utils.is_iscsi_boot(task)) + + def test_is_iscsi_boot_exception(self): + self.node.driver_internal_info = { + 'boot_from_volume': uuidutils.generate_uuid()} + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + self.assertFalse(utils.is_iscsi_boot(task)) + + def test_is_iscsi_boot_false(self): + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + self.assertFalse(utils.is_iscsi_boot(task)) + + def test_is_iscsi_boot_false_fc_target(self): + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='fibre_channel', + boot_index=0, volume_id='3214', uuid=vol_id) + self.node.driver_internal_info.update({'boot_from_volume': vol_id}) + self.node.save() + with task_manager.acquire( + self.context, self.node.uuid, shared=False) as task: + self.assertFalse(utils.is_iscsi_boot(task)) diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index 8f65263d7d..fe5de609fb 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -285,7 +285,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): whle_dsk_img=False, ipxe_timeout=0, ipxe_use_swift=False, - debug=False): + debug=False, + boot_from_volume=False): self.config(debug=debug) self.config(pxe_append_params='test_param', group='pxe') # NOTE: right '/' should be removed from url string @@ -370,6 +371,17 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): 'ipxe_timeout': ipxe_timeout_in_ms, } + if boot_from_volume: + expected_options.update({ + 'boot_from_volume': True, + 'iscsi_boot_url': 'iscsi:fake_host::3260:0:fake_iqn', + 'iscsi_initiator_iqn': 'fake_iqn', + 'iscsi_volumes': ['iscsi:fake_host::3260:1:fake_iqn'], + 'username': 'fake_username', + 'password': 'fake_password'}) + expected_options.pop('deployment_aki_path') + expected_options.pop('deployment_ari_path') + with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: options = pxe._build_pxe_config_options(task, image_info) @@ -401,6 +413,121 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): self._test_build_pxe_config_options_ipxe(whle_dsk_img=True, ipxe_timeout=120) + def test__build_pxe_config_options_ipxe_and_iscsi_boot(self): + vol_id = uuidutils.generate_uuid() + vol_id2 = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': 0, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn', + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=1, volume_id='1235', uuid=vol_id2, + properties={'target_lun': 1, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn'}) + self.node.driver_internal_info.update({'boot_from_volume': vol_id}) + self._test_build_pxe_config_options_ipxe(boot_from_volume=True) + + def test__build_pxe_config_options_ipxe_and_iscsi_boot_from_lists(self): + vol_id = uuidutils.generate_uuid() + vol_id2 = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_luns': [0, 2], + 'target_portals': ['fake_host:3260', + 'faker_host:3261'], + 'target_iqns': ['fake_iqn', 'faker_iqn'], + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=1, volume_id='1235', uuid=vol_id2, + properties={'target_lun': [1, 3], + 'target_portal': ['fake_host:3260', 'faker_host:3261'], + 'target_iqn': ['fake_iqn', 'faker_iqn']}) + self.node.driver_internal_info.update({'boot_from_volume': vol_id}) + self._test_build_pxe_config_options_ipxe(boot_from_volume=True) + + def test__get_volume_pxe_options(self): + vol_id = uuidutils.generate_uuid() + vol_id2 = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': [0, 1, 3], + 'target_portal': 'fake_host:3260', + 'target_iqns': 'fake_iqn', + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=1, volume_id='1235', uuid=vol_id2, + properties={'target_lun': 1, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn'}) + self.node.driver_internal_info.update({'boot_from_volume': vol_id}) + driver_internal_info = self.node.driver_internal_info + driver_internal_info['boot_from_volume'] = vol_id + self.node.driver_internal_info = driver_internal_info + self.node.save() + + expected = {'boot_from_volume': True, + 'username': 'fake_username', 'password': 'fake_password', + 'iscsi_boot_url': 'iscsi:fake_host::3260:0:fake_iqn', + 'iscsi_initiator_iqn': 'fake_iqn', + 'iscsi_volumes': ['iscsi:fake_host::3260:1:fake_iqn']} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + options = pxe._get_volume_pxe_options(task) + self.assertEqual(expected, options) + + def test__get_volume_pxe_options_unsupported_volume_type(self): + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='fake_type', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'foo': 'bar'}) + + driver_internal_info = self.node.driver_internal_info + driver_internal_info['boot_from_volume'] = vol_id + self.node.driver_internal_info = driver_internal_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + options = pxe._get_volume_pxe_options(task) + self.assertEqual({}, options) + + def test__get_volume_pxe_options_unsupported_additional_volume_type(self): + vol_id = uuidutils.generate_uuid() + vol_id2 = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': 0, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn', + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='fake_type', + boot_index=1, volume_id='1234', uuid=vol_id2, + properties={'foo': 'bar'}) + + driver_internal_info = self.node.driver_internal_info + driver_internal_info['boot_from_volume'] = vol_id + self.node.driver_internal_info = driver_internal_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + options = pxe._get_volume_pxe_options(task) + self.assertEqual([], options['iscsi_volumes']) + @mock.patch.object(deploy_utils, 'fetch_images', autospec=True) def test__cache_tftp_images_master_path(self, mock_fetch_image): temp_dir = tempfile.mkdtemp() @@ -876,7 +1003,7 @@ class PXEBootTestCase(db_base.DbTestCase): provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False) + 'bios', False, False, False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE) @@ -918,7 +1045,7 @@ class PXEBootTestCase(db_base.DbTestCase): task, mock.ANY, CONF.pxe.pxe_config_template) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False) + 'bios', False, False, False) self.assertFalse(set_boot_device_mock.called) @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True)