Merge "Allow using TempURLs for deploy images"

This commit is contained in:
Jenkins 2016-09-19 16:48:28 +00:00 committed by Gerrit Code Review
commit f41c26d4f2
6 changed files with 240 additions and 127 deletions

View File

@ -212,6 +212,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}
@ -743,6 +745,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

@ -2758,6 +2758,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

@ -30,6 +30,7 @@ from ironic.common import exception
from ironic.common.glance_service import service_utils
from ironic.common.i18n import _, _LE, _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
@ -81,11 +82,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
@ -125,6 +130,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
@ -139,47 +186,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
@ -324,7 +345,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']
@ -386,9 +408,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.