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 <juliaashleykreger@gmail.com>
Co-Authored-By: Joanna Taryma <joanna.taryma@intel.com>
Co-Authored-By: Michael Turek <mjturek@linux.vnet.ibm.com>
Change-Id: I75869262dbfd1caa779fa21e93cdb31f193cb829
Partial-Bug: #1559691
This commit is contained in:
Joanna Taryma 2017-04-10 16:01:44 -07:00 committed by Michael Turek
parent cbedc8268c
commit 6883674b3c
8 changed files with 424 additions and 8 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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)