From 133dac255fc6af0cd91afeccf4690df3c999fdc6 Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 24 Mar 2021 15:23:24 +0100 Subject: [PATCH] Allow overriding an external URL for virtual media Virtual media deployments can be conducted outside of the provisioning network as long as the node gets an IP address somehow and can reach ironic and its HTTP server. This changes adds new configuration that allows to use public IP addresses for virtual media while keeping PXE boots working and constrained to the provisioning network. Change-Id: I8b859b2812160ff3911eb7d648eab835ef61d934 Story: #2008566 Task: #41706 --- doc/source/admin/dhcp-less.rst | 23 ++++++ doc/source/admin/interfaces/deploy.rst | 3 + doc/source/install/configure-pxe.rst | 2 + ironic/conf/deploy.py | 11 +++ ironic/drivers/modules/deploy_utils.py | 5 +- ironic/drivers/modules/image_utils.py | 17 ++++- .../unit/drivers/modules/test_image_utils.py | 70 ++++++++++++++++++- .../notes/external-ip-5ec9b7b55a90cec4.yaml | 13 ++++ 8 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/external-ip-5ec9b7b55a90cec4.yaml diff --git a/doc/source/admin/dhcp-less.rst b/doc/source/admin/dhcp-less.rst index c9a0d9608b..fe3aa055f9 100644 --- a/doc/source/admin/dhcp-less.rst +++ b/doc/source/admin/dhcp-less.rst @@ -87,3 +87,26 @@ An example network data: .. _Glean: https://docs.openstack.org/infra/glean/ .. _simple-init: https://docs.openstack.org/diskimage-builder/latest/elements/simple-init/README.html .. _network_data: https://specs.openstack.org/openstack/nova-specs/specs/liberty/implemented/metadata-service-network-info.html + +.. _l3-external-ip: + +Deploying outside of the provisioning network +--------------------------------------------- + +If you need to combine traditional deployments using a provisioning network +with virtual media deployments over L3, you may need to provide an alternative +IP address for the remote nodes to connect to: + +.. code-block:: ini + + [deploy] + http_url = + external_http_url = + +You may also need to override the callback URL, which is normally fetched from +the service catalog or configured in the ``[service_catalog]`` section: + +.. code-block:: ini + + [deploy] + external_callback_url = diff --git a/doc/source/admin/interfaces/deploy.rst b/doc/source/admin/interfaces/deploy.rst index d7884d8163..f8fbb39163 100644 --- a/doc/source/admin/interfaces/deploy.rst +++ b/doc/source/admin/interfaces/deploy.rst @@ -71,6 +71,9 @@ ironic configuration file to match the HTTP server configurations. http_url = http://example.com http_root = /httpboot +.. note:: + See also: :ref:`l3-external-ip`. + Each HTTP server should be configured to follow symlinks for images accessible from HTTP service. Please refer to configuration option ``FollowSymLinks`` if you are using Apache HTTP server, or diff --git a/doc/source/install/configure-pxe.rst b/doc/source/install/configure-pxe.rst index 6c51a24f35..0f5f392a2f 100644 --- a/doc/source/install/configure-pxe.rst +++ b/doc/source/install/configure-pxe.rst @@ -332,6 +332,8 @@ on the Bare Metal service node(s) where ``ironic-conductor`` is running. # http://192.1.2.3:8080 (string value) http_url=http://192.168.0.2:8080 + See also: :ref:`l3-external-ip`. + #. Install the iPXE package with the boot images: Ubuntu:: diff --git a/ironic/conf/deploy.py b/ironic/conf/deploy.py index 49975595cd..69d5fa537d 100644 --- a/ironic/conf/deploy.py +++ b/ironic/conf/deploy.py @@ -27,6 +27,17 @@ opts = [ cfg.StrOpt('http_root', default='/httpboot', help=_("ironic-conductor node's HTTP root path.")), + cfg.StrOpt('external_http_url', + help=_("URL of the ironic-conductor node's HTTP server for " + "boot methods such as virtual media, " + "where images could be served outside of the " + "provisioning network. Does not apply when Swift is " + "used. Defaults to http_url.")), + cfg.StrOpt('external_callback_url', + help=_("Agent callback URL of the bare metal API for boot " + "methods such as virtual media, where images could be " + "served outside of the provisioning network. Defaults " + "to the configuration from [service_catalog].")), cfg.BoolOpt('enable_ata_secure_erase', default=True, mutable=True, diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 432bddbabe..2e225d5699 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -617,6 +617,9 @@ def is_software_raid(node): return software_raid +IPA_URL_PARAM_NAME = 'ipa-api-url' + + def build_agent_options(node): """Build the options to be passed to the agent ramdisk. @@ -625,7 +628,7 @@ def build_agent_options(node): agent ramdisk. """ agent_config_opts = { - 'ipa-api-url': get_ironic_api_url(), + IPA_URL_PARAM_NAME: get_ironic_api_url(), } return agent_config_opts diff --git a/ironic/drivers/modules/image_utils.py b/ironic/drivers/modules/image_utils.py index e26ecddf1c..f1283b092a 100644 --- a/ironic/drivers/modules/image_utils.py +++ b/ironic/drivers/modules/image_utils.py @@ -214,8 +214,8 @@ class ImageHandler(object): shutil.copyfile(image_file, published_file) os.chmod(published_file, self._file_permission) - image_url = os.path.join( - CONF.deploy.http_url, self._image_subdir, object_name) + http_url = CONF.deploy.external_http_url or CONF.deploy.http_url + image_url = os.path.join(http_url, self._image_subdir, object_name) image_url = self._append_filename_param( image_url, os.path.basename(image_file)) @@ -244,6 +244,16 @@ def cleanup_iso_image(task): suffix='.iso') +def override_api_url(params): + if not CONF.deploy.external_callback_url: + return params + + params = params or {} + params[deploy_utils.IPA_URL_PARAM_NAME] = \ + CONF.deploy.external_callback_url.rstrip('/') + return params + + def prepare_floppy_image(task, params=None): """Prepares the floppy image for passing the parameters. @@ -264,6 +274,7 @@ def prepare_floppy_image(task, params=None): :returns: image URL for the floppy image. """ object_name = _get_name(task.node, prefix='image') + params = override_api_url(params) LOG.debug("Trying to create floppy image for node " "%(node)s", {'node': task.node.uuid}) @@ -533,6 +544,8 @@ def prepare_deploy_iso(task, params, mode, d_info): iso_href = _find_param(iso_str, d_info) bootloader_href = _find_param(bootloader_str, d_info) + params = override_api_url(params) + # TODO(TheJulia): At some point we should support something like # boot_iso for the deploy interface, perhaps when we support config # injection. diff --git a/ironic/tests/unit/drivers/modules/test_image_utils.py b/ironic/tests/unit/drivers/modules/test_image_utils.py index a8305d0233..cb41db3eb0 100644 --- a/ironic/tests/unit/drivers/modules/test_image_utils.py +++ b/ironic/tests/unit/drivers/modules/test_image_utils.py @@ -121,6 +121,28 @@ class RedfishImageHandlerTestCase(db_base.DbTestCase): 'file.iso', '/httpboot/redfish/boot.iso') mock_chmod.assert_called_once_with('file.iso', 0o644) + @mock.patch.object(os, 'chmod', autospec=True) + @mock.patch.object(image_utils, 'shutil', autospec=True) + @mock.patch.object(os, 'link', autospec=True) + @mock.patch.object(os, 'mkdir', autospec=True) + def test_publish_image_external_ip( + self, mock_mkdir, mock_link, mock_shutil, mock_chmod): + self.config(use_swift=False, group='redfish') + self.config(http_url='http://localhost', + external_http_url='http://non-local.host', + group='deploy') + img_handler_obj = image_utils.ImageHandler(self.node.driver) + + url = img_handler_obj.publish_image('file.iso', 'boot.iso') + + self.assertEqual( + 'http://non-local.host/redfish/boot.iso?filename=file.iso', url) + + mock_mkdir.assert_called_once_with('/httpboot/redfish', 0o755) + mock_link.assert_called_once_with( + 'file.iso', '/httpboot/redfish/boot.iso') + mock_chmod.assert_called_once_with('file.iso', 0o644) + @mock.patch.object(os, 'chmod', autospec=True) @mock.patch.object(image_utils, 'shutil', autospec=True) @mock.patch.object(os, 'link', autospec=True) @@ -248,7 +270,31 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): mock.ANY, object_name) mock_create_vfat_image.assert_called_once_with( - mock.ANY, parameters=mock.ANY) + mock.ANY, parameters=None) + + self.assertEqual(expected_url, url) + + @mock.patch.object(image_utils.ImageHandler, 'publish_image', + autospec=True) + @mock.patch.object(images, 'create_vfat_image', autospec=True) + def test_prepare_floppy_image_with_external_ip( + self, mock_create_vfat_image, mock_publish_image): + self.config(external_callback_url='http://callback/', group='deploy') + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + expected_url = 'https://a.b/c.f?e=f' + + mock_publish_image.return_value = expected_url + + url = image_utils.prepare_floppy_image(task) + + object_name = 'image-%s' % task.node.uuid + + mock_publish_image.assert_called_once_with(mock.ANY, + mock.ANY, object_name) + + mock_create_vfat_image.assert_called_once_with( + mock.ANY, parameters={"ipa-api-url": "http://callback"}) self.assertEqual(expected_url, url) @@ -632,6 +678,28 @@ cafile = /etc/ironic-python-agent/ironic.crt task, 'kernel', 'ramdisk', 'bootloader', params={}, inject_files=expected_files, base_iso=None) + @mock.patch.object(image_utils, '_prepare_iso_image', autospec=True) + def test_prepare_deploy_iso_external_ip(self, mock__prepare_iso_image): + self.config(external_callback_url='http://callback/', group='deploy') + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + d_info = { + 'ilo_deploy_kernel': 'kernel', + 'ilo_deploy_ramdisk': 'ramdisk', + 'ilo_bootloader': 'bootloader' + } + task.node.driver_info.update(d_info) + + task.node.instance_info.update(deploy_boot_mode='uefi') + + image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info) + + mock__prepare_iso_image.assert_called_once_with( + task, 'kernel', 'ramdisk', 'bootloader', + params={'ipa-api-url': 'http://callback'}, + inject_files={}, base_iso=None) + @mock.patch.object(image_utils, '_find_param', autospec=True) @mock.patch.object(image_utils, '_prepare_iso_image', autospec=True) @mock.patch.object(images, 'create_boot_iso', autospec=True) diff --git a/releasenotes/notes/external-ip-5ec9b7b55a90cec4.yaml b/releasenotes/notes/external-ip-5ec9b7b55a90cec4.yaml new file mode 100644 index 0000000000..eea112ba39 --- /dev/null +++ b/releasenotes/notes/external-ip-5ec9b7b55a90cec4.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Provides operator ability to override URL settings required for + provisioning/cleaning in the event of virtual media based deployment. + These scenarios tend to require more delineation than more traditional + deployments as they often have a different environmental security + requirements. Set these two new configuration options using an IP + address that is available to these nodes (both the ramdisk and the BMCs):: + + [deploy] + external_http_url = + external_callback_url =