Allow using TempURLs for deploy images

when iPXE is enabled, it is possible for the bootloader to download
the deploy kernel and ramdisk directly from Swift TempURL instead of
downloading them to conductor and serving from local HTTP server.

This patch adds the required logic and a new config option
`ipxe_use_swift` (default False), setting which to True enables
using Swift TempURLs for deploy ramdisk and kernel.

Note that local caching and serving for kernel and ramdisk of user image
is still performed for partition images that require non-local boot,
as moving those to use TempURLs will make it impossible for the user
to reboot the instance when TempURLs time out or image is deleted from
Glance/Swift.

Change-Id: I106cc6148c329e784bfbb5019fdfeb0509a9de09
Closes-Bug: #1526404
Co-Authored-By: Andrey Shestakov <ashestakov@mirantis.com>
This commit is contained in:
Pavlo Shchelokovskyy 2016-08-18 12:06:18 +03:00
parent 4ce008e8ff
commit 497be96d4a
6 changed files with 240 additions and 127 deletions

View File

@ -208,6 +208,8 @@ IRONIC_HOSTPORT=${IRONIC_HOSTPORT:-$SERVICE_HOST:$IRONIC_SERVICE_PORT}
# Enable iPXE
IRONIC_IPXE_ENABLED=$(trueorfalse True IRONIC_IPXE_ENABLED)
# Options below are only applied when IRONIC_IPXE_ENABLED is True
IRONIC_IPXE_USE_SWIFT=$(trueorfalse False IRONIC_IPXE_USE_SWIFT)
IRONIC_HTTP_DIR=${IRONIC_HTTP_DIR:-$IRONIC_DATA_DIR/httpboot}
IRONIC_HTTP_SERVER=${IRONIC_HTTP_SERVER:-$IRONIC_TFTPSERVER_IP}
IRONIC_HTTP_PORT=${IRONIC_HTTP_PORT:-3928}
@ -690,6 +692,9 @@ function configure_ironic_conductor {
iniset $IRONIC_CONF_FILE pxe pxe_bootfile_name $pxebin
iniset $IRONIC_CONF_FILE deploy http_root $IRONIC_HTTP_DIR
iniset $IRONIC_CONF_FILE deploy http_url "http://$IRONIC_HTTP_SERVER:$IRONIC_HTTP_PORT"
if [[ "$IRONIC_IPXE_USE_SWIFT" == "True" ]]; then
iniset $IRONIC_CONF_FILE pxe ipxe_use_swift True
fi
fi
if [[ "$IRONIC_IS_HARDWARE" == "False" ]]; then

View File

@ -2621,6 +2621,13 @@
# Allowed values: 4, 6
#ip_version = 4
# Download deploy images directly from swift using temporary
# URLs. If set to false (default), images are downloaded to
# the ironic-conductor node and served over its local HTTP
# server. Applicable only when 'ipxe_enabled' option is set to
# true. (boolean value)
#ipxe_use_swift = false
[seamicro]

View File

@ -96,6 +96,16 @@ opts = [
choices=['4', '6'],
help=_('The IP version that will be used for PXE booting. '
'Defaults to 4. EXPERIMENTAL')),
cfg.BoolOpt('ipxe_use_swift',
default=False,
help=_("Download deploy images directly from swift using "
"temporary URLs. "
"If set to false (default), images are downloaded "
"to the ironic-conductor node and served over its "
"local HTTP server. "
"Applicable only when 'ipxe_enabled' option is "
"set to true.")),
]

View File

@ -32,6 +32,7 @@ from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common.i18n import _LW
from ironic.common import image_service as service
from ironic.common import images
from ironic.common import pxe_utils
from ironic.common import states
from ironic.conf import CONF
@ -83,11 +84,15 @@ def _get_instance_image_info(node, ctx):
:param ctx: context
:returns: a dictionary whose keys are the names of the images (kernel,
ramdisk) and values are the absolute paths of them. If it's a whole
disk image, it returns an empty dictionary.
disk image or node is configured for localboot,
it returns an empty dictionary.
"""
image_info = {}
if node.driver_internal_info.get('is_whole_disk_image'):
return image_info
# NOTE(pas-ha) do not report image kernel and ramdisk for
# local boot or whole disk images so that they are not cached
if (node.driver_internal_info.get('is_whole_disk_image') or
deploy_utils.get_boot_option(node) == 'local'):
return image_info
root_dir = pxe_utils.get_root_dir()
i_info = node.instance_info
@ -127,6 +132,48 @@ def _get_deploy_image_info(node):
return pxe_utils.get_deploy_kr_info(node.uuid, d_info)
def _get_pxe_kernel_ramdisk(pxe_info):
pxe_opts = {}
pxe_opts['deployment_aki_path'] = pxe_info['deploy_kernel'][1]
pxe_opts['deployment_ari_path'] = pxe_info['deploy_ramdisk'][1]
# It is possible that we don't have kernel/ramdisk or even
# image_source to determine if it's a whole disk image or not.
# For example, when transitioning to 'available' state for first
# time from 'manage' state.
if 'kernel' in pxe_info:
pxe_opts['aki_path'] = pxe_info['kernel'][1]
if 'ramdisk' in pxe_info:
pxe_opts['ari_path'] = pxe_info['ramdisk'][1]
return pxe_opts
def _get_ipxe_kernel_ramdisk(task, pxe_info):
pxe_opts = {}
node = task.node
for label, option in (('deploy_kernel', 'deployment_aki_path'),
('deploy_ramdisk', 'deployment_ari_path')):
image_href = pxe_info[label][0]
if (CONF.pxe.ipxe_use_swift and
service_utils.is_glance_image(image_href)):
pxe_opts[option] = images.get_temp_url_for_glance_image(
task.context, image_href)
else:
pxe_opts[option] = '/'.join([CONF.deploy.http_url, node.uuid,
label])
# NOTE(pas-ha) do not use Swift TempURLs for kernel and ramdisk
# of user image when boot_option is not local,
# as this will break instance reboot later when temp urls have timed out.
if 'kernel' in pxe_info:
pxe_opts['aki_path'] = '/'.join(
[CONF.deploy.http_url, node.uuid, 'kernel'])
if 'ramdisk' in pxe_info:
pxe_opts['ari_path'] = '/'.join(
[CONF.deploy.http_url, node.uuid, 'ramdisk'])
return pxe_opts
def _build_pxe_config_options(task, pxe_info):
"""Build the PXE config options for a node
@ -141,47 +188,21 @@ def _build_pxe_config_options(task, pxe_info):
:returns: A dictionary of pxe options to be used in the pxe bootfile
template.
"""
node = task.node
is_whole_disk_image = node.driver_internal_info.get('is_whole_disk_image')
if CONF.pxe.ipxe_enabled:
pxe_options = _get_ipxe_kernel_ramdisk(task, pxe_info)
else:
pxe_options = _get_pxe_kernel_ramdisk(pxe_info)
# These are dummy values to satisfy elilo.
# image and initrd fields in elilo config cannot be blank.
kernel = 'no_kernel'
ramdisk = 'no_ramdisk'
pxe_options.setdefault('aki_path', 'no_kernel')
pxe_options.setdefault('ari_path', 'no_ramdisk')
if CONF.pxe.ipxe_enabled:
deploy_kernel = '/'.join([CONF.deploy.http_url, node.uuid,
'deploy_kernel'])
deploy_ramdisk = '/'.join([CONF.deploy.http_url, node.uuid,
'deploy_ramdisk'])
if not is_whole_disk_image:
kernel = '/'.join([CONF.deploy.http_url, node.uuid,
'kernel'])
ramdisk = '/'.join([CONF.deploy.http_url, node.uuid,
'ramdisk'])
else:
deploy_kernel = pxe_info['deploy_kernel'][1]
deploy_ramdisk = pxe_info['deploy_ramdisk'][1]
if not is_whole_disk_image:
# It is possible that we don't have kernel/ramdisk or even
# image_source to determine if it's a whole disk image or not.
# For example, when transitioning to 'available' state for first
# time from 'manage' state. Retain dummy values if we don't have
# kernel/ramdisk.
if 'kernel' in pxe_info:
kernel = pxe_info['kernel'][1]
if 'ramdisk' in pxe_info:
ramdisk = pxe_info['ramdisk'][1]
pxe_options = {
'deployment_aki_path': deploy_kernel,
'deployment_ari_path': deploy_ramdisk,
pxe_options.update({
'pxe_append_params': CONF.pxe.pxe_append_params,
'tftp_server': CONF.pxe.tftp_server,
'aki_path': kernel,
'ari_path': ramdisk,
'ipxe_timeout': CONF.pxe.ipxe_timeout * 1000
}
})
return pxe_options
@ -326,7 +347,8 @@ class PXEBoot(base.BootInterface):
_parse_driver_info(node)
d_info = deploy_utils.get_image_instance_info(node)
if node.driver_internal_info.get('is_whole_disk_image'):
if (node.driver_internal_info.get('is_whole_disk_image') or
deploy_utils.get_boot_option(node) == 'local'):
props = []
elif service_utils.is_glance_image(d_info['image_source']):
props = ['kernel_id', 'ramdisk_id']
@ -388,9 +410,11 @@ class PXEBoot(base.BootInterface):
pxe_config_template)
deploy_utils.try_set_boot_device(task, boot_devices.PXE)
# 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, node, pxe_info)
if CONF.pxe.ipxe_enabled and CONF.pxe.ipxe_use_swift:
pxe_info.pop('deploy_kernel', None)
pxe_info.pop('deploy_ramdisk', None)
if pxe_info:
_cache_ramdisk_kernel(task.context, node, pxe_info)
@METRICS.timer('PXEBoot.clean_up_ramdisk')
def clean_up_ramdisk(self, task):

View File

@ -19,6 +19,7 @@ import filecmp
import os
import shutil
import tempfile
import uuid
from ironic_lib import utils as ironic_utils
import mock
@ -145,6 +146,14 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
self.node.save()
self._test__get_instance_image_info()
@mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option',
return_value='local')
def test__get_instance_image_info_localboot(self, boot_opt_mock):
self.node.driver_internal_info['is_whole_disk_image'] = False
self.node.save()
image_info = pxe._get_instance_image_info(self.node, self.context)
self.assertEqual({}, image_info)
@mock.patch.object(base_image_service.BaseImageService, '_show',
autospec=True)
def test__get_instance_image_info_whole_disk_image(self, show_mock):
@ -154,11 +163,14 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
image_info = pxe._get_instance_image_info(self.node, self.context)
self.assertEqual({}, image_info)
@mock.patch('ironic.common.image_service.GlanceImageService',
autospec=True)
@mock.patch.object(pxe_utils, '_build_pxe_config', autospec=True)
def _test_build_pxe_config_options(self, build_pxe_mock,
def _test_build_pxe_config_options(self, build_pxe_mock, glance_mock,
whle_dsk_img=False,
ipxe_enabled=False,
ipxe_timeout=0):
ipxe_timeout=0,
ipxe_use_swift=False):
self.config(pxe_append_params='test_param', group='pxe')
# NOTE: right '/' should be removed from url string
self.config(api_url='http://192.168.122.184:6385', group='conductor')
@ -175,11 +187,18 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
http_url = 'http://192.1.2.3:1234'
self.config(ipxe_enabled=True, group='pxe')
self.config(http_url=http_url, group='deploy')
deploy_kernel = os.path.join(http_url, self.node.uuid,
'deploy_kernel')
deploy_ramdisk = os.path.join(http_url, self.node.uuid,
'deploy_ramdisk')
if ipxe_use_swift:
self.config(ipxe_use_swift=True, group='pxe')
glance = mock.Mock()
glance_mock.return_value = glance
glance.swift_temp_url.side_effect = [
deploy_kernel, deploy_ramdisk] = [
'swift_kernel', 'swift_ramdisk']
else:
deploy_kernel = os.path.join(http_url, self.node.uuid,
'deploy_kernel')
deploy_ramdisk = os.path.join(http_url, self.node.uuid,
'deploy_ramdisk')
kernel = os.path.join(http_url, self.node.uuid, 'kernel')
ramdisk = os.path.join(http_url, self.node.uuid, 'ramdisk')
root_dir = CONF.deploy.http_root
@ -194,9 +213,44 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
'ramdisk')
root_dir = CONF.pxe.tftp_root
if whle_dsk_img:
ramdisk = 'no_ramdisk'
kernel = 'no_kernel'
if ipxe_use_swift:
image_info = {
'deploy_kernel': (str(uuid.uuid4()),
os.path.join(root_dir,
self.node.uuid,
'deploy_kernel')),
'deploy_ramdisk': (str(uuid.uuid4()),
os.path.join(root_dir,
self.node.uuid,
'deploy_ramdisk'))
}
else:
image_info = {
'deploy_kernel': ('deploy_kernel',
os.path.join(root_dir,
self.node.uuid,
'deploy_kernel')),
'deploy_ramdisk': ('deploy_ramdisk',
os.path.join(root_dir,
self.node.uuid,
'deploy_ramdisk'))
}
if (whle_dsk_img or
deploy_utils.get_boot_option(self.node) == 'local'):
ramdisk = 'no_ramdisk'
kernel = 'no_kernel'
else:
image_info.update({
'kernel': ('kernel_id',
os.path.join(root_dir,
self.node.uuid,
'kernel')),
'ramdisk': ('ramdisk_id',
os.path.join(root_dir,
self.node.uuid,
'ramdisk'))
})
ipxe_timeout_in_ms = ipxe_timeout * 1000
@ -210,23 +264,6 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
'ipxe_timeout': ipxe_timeout_in_ms,
}
image_info = {'deploy_kernel': ('deploy_kernel',
os.path.join(root_dir,
self.node.uuid,
'deploy_kernel')),
'deploy_ramdisk': ('deploy_ramdisk',
os.path.join(root_dir,
self.node.uuid,
'deploy_ramdisk')),
'kernel': ('kernel_id',
os.path.join(root_dir,
self.node.uuid,
'kernel')),
'ramdisk': ('ramdisk_id',
os.path.join(root_dir,
self.node.uuid,
'ramdisk'))}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
options = pxe._build_pxe_config_options(task, image_info)
@ -236,10 +273,38 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
self._test_build_pxe_config_options(whle_dsk_img=True,
ipxe_enabled=False)
def test__build_pxe_config_options_local_boot(self):
del self.node.driver_internal_info['is_whole_disk_image']
i_info = self.node.instance_info
i_info.update({'capabilities': {'boot_option': 'local'}})
self.node.instance_info = i_info
self.node.save()
self._test_build_pxe_config_options(whle_dsk_img=False,
ipxe_enabled=False)
def test__build_pxe_config_options_ipxe(self):
self._test_build_pxe_config_options(whle_dsk_img=True,
ipxe_enabled=True)
def test__build_pxe_config_options_ipxe_local_boot(self):
del self.node.driver_internal_info['is_whole_disk_image']
i_info = self.node.instance_info
i_info.update({'capabilities': {'boot_option': 'local'}})
self.node.instance_info = i_info
self.node.save()
self._test_build_pxe_config_options(whle_dsk_img=False,
ipxe_enabled=True)
def test__build_pxe_config_options_ipxe_swift_wdi(self):
self._test_build_pxe_config_options(whle_dsk_img=True,
ipxe_enabled=True,
ipxe_use_swift=True)
def test__build_pxe_config_options_ipxe_swift_partition(self):
self._test_build_pxe_config_options(whle_dsk_img=False,
ipxe_enabled=True,
ipxe_use_swift=True)
def test__build_pxe_config_options_without_is_whole_disk_image(self):
del self.node.driver_internal_info['is_whole_disk_image']
self.node.save()
@ -251,62 +316,6 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
ipxe_enabled=True,
ipxe_timeout=120)
@mock.patch.object(pxe_utils, '_build_pxe_config', autospec=True)
def test__build_pxe_config_options_whole_disk_image(self,
build_pxe_mock,
ipxe_enabled=False):
self.config(pxe_append_params='test_param', group='pxe')
# NOTE: right '/' should be removed from url string
self.config(api_url='http://192.168.122.184:6385', group='conductor')
tftp_server = CONF.pxe.tftp_server
if ipxe_enabled:
http_url = 'http://192.1.2.3:1234'
self.config(ipxe_enabled=True, group='pxe')
self.config(http_url=http_url, group='deploy')
deploy_kernel = os.path.join(http_url, self.node.uuid,
'deploy_kernel')
deploy_ramdisk = os.path.join(http_url, self.node.uuid,
'deploy_ramdisk')
root_dir = CONF.deploy.http_root
else:
deploy_kernel = os.path.join(CONF.pxe.tftp_root, self.node.uuid,
'deploy_kernel')
deploy_ramdisk = os.path.join(CONF.pxe.tftp_root, self.node.uuid,
'deploy_ramdisk')
root_dir = CONF.pxe.tftp_root
expected_options = {
'deployment_ari_path': deploy_ramdisk,
'pxe_append_params': 'test_param',
'deployment_aki_path': deploy_kernel,
'tftp_server': tftp_server,
'aki_path': 'no_kernel',
'ari_path': 'no_ramdisk',
'ipxe_timeout': 0,
}
image_info = {'deploy_kernel': ('deploy_kernel',
os.path.join(root_dir,
self.node.uuid,
'deploy_kernel')),
'deploy_ramdisk': ('deploy_ramdisk',
os.path.join(root_dir,
self.node.uuid,
'deploy_ramdisk')),
}
driver_internal_info = self.node.driver_internal_info
driver_internal_info['is_whole_disk_image'] = True
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._build_pxe_config_options(task, image_info)
self.assertEqual(expected_options, options)
def test__build_pxe_config_options_no_kernel_no_ramdisk(self):
del self.node.driver_internal_info['is_whole_disk_image']
self.node.save()
@ -655,20 +664,38 @@ class PXEBootTestCase(db_base.DbTestCase):
mock_deploy_img_info,
mock_instance_img_info,
dhcp_factory_mock, uefi=False,
cleaning=False):
cleaning=False,
ipxe_use_swift=False,
whole_disk_image=False):
mock_build_pxe.return_value = {}
mock_deploy_img_info.return_value = {'deploy_kernel': 'a'}
mock_instance_img_info.return_value = {'kernel': 'b'}
if whole_disk_image:
mock_instance_img_info.return_value = {}
else:
mock_instance_img_info.return_value = {'kernel': 'b'}
mock_pxe_config.return_value = None
mock_cache_r_k.return_value = None
provider_mock = mock.MagicMock()
dhcp_factory_mock.return_value = provider_mock
driver_internal_info = self.node.driver_internal_info
driver_internal_info['is_whole_disk_image'] = whole_disk_image
self.node.driver_internal_info = driver_internal_info
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'})
mock_deploy_img_info.assert_called_once_with(task.node)
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
if cleaning is False:
if ipxe_use_swift:
if whole_disk_image:
self.assertFalse(mock_cache_r_k.called)
else:
mock_cache_r_k.assert_called_once_with(
self.context, task.node,
{'kernel': 'b'})
mock_instance_img_info.assert_called_once_with(task.node,
self.context)
elif cleaning is False:
mock_cache_r_k.assert_called_once_with(
self.context, task.node,
{'deploy_kernel': 'a', 'kernel': 'b'})
@ -749,6 +776,34 @@ class PXEBootTestCase(db_base.DbTestCase):
self._test_prepare_ramdisk()
self.assertFalse(copyfile_mock.called)
@mock.patch.object(shutil, 'copyfile', autospec=True)
def test_prepare_ramdisk_ipxe_swift(self, copyfile_mock):
self.node.provision_state = states.DEPLOYING
self.node.save()
self.config(group='pxe', ipxe_enabled=True)
self.config(group='pxe', ipxe_use_swift=True)
self.config(group='deploy', http_url='http://myserver')
self._test_prepare_ramdisk(ipxe_use_swift=True)
copyfile_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script,
os.path.join(
CONF.deploy.http_root,
os.path.basename(CONF.pxe.ipxe_boot_script)))
@mock.patch.object(shutil, 'copyfile', autospec=True)
def test_prepare_ramdisk_ipxe_swift_whole_disk_image(self, copyfile_mock):
self.node.provision_state = states.DEPLOYING
self.node.save()
self.config(group='pxe', ipxe_enabled=True)
self.config(group='pxe', ipxe_use_swift=True)
self.config(group='deploy', http_url='http://myserver')
self._test_prepare_ramdisk(ipxe_use_swift=True, whole_disk_image=True)
copyfile_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script,
os.path.join(
CONF.deploy.http_root,
os.path.basename(CONF.pxe.ipxe_boot_script)))
def test_prepare_ramdisk_cleaning(self):
self.node.provision_state = states.CLEANING
self.node.save()

View File

@ -0,0 +1,12 @@
---
features:
- By default, the ironic-conductor service caches the node's deploy
ramdisk and kernel images locally and serves them via a separate
HTTP server. A new ``[pxe]ipxe_use_swift`` configuration option
(disabled by default) allows images to be accessed directly
from object store via Swift temporary URLs.
This is only applicable if iPXE is enabled (via ``[pxe]ipxe_enabled``
configuration option) and image store is in Glance/Swift.
For user images that are partition images requiring non-local boot,
the default behavior with local caching and an HTTP server
will still apply for user image kernel and ramdisk.