diff --git a/devstack/lib/ironic b/devstack/lib/ironic index 12aaa5808..ff56411b0 100644 --- a/devstack/lib/ironic +++ b/devstack/lib/ironic @@ -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 diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 5a2815c0c..0662abfe7 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -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] diff --git a/ironic/conf/pxe.py b/ironic/conf/pxe.py index 7e16724ec..e17a919d7 100644 --- a/ironic/conf/pxe.py +++ b/ironic/conf/pxe.py @@ -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.")), + ] diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index 7e844c0de..1860a8a43 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -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): diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index 3f314c486..3e67a0341 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -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() diff --git a/releasenotes/notes/ipxe-use-swift-5ccf490daab809cc.yaml b/releasenotes/notes/ipxe-use-swift-5ccf490daab809cc.yaml new file mode 100644 index 000000000..dc6fe708d --- /dev/null +++ b/releasenotes/notes/ipxe-use-swift-5ccf490daab809cc.yaml @@ -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.