From 880bd639f38ba8128e4fb703be0b4a773af83607 Mon Sep 17 00:00:00 2001 From: Arun S A G Date: Tue, 16 Feb 2021 02:16:32 -0800 Subject: [PATCH] Add anaconda support in the pxe boot driver To prepare for booting anaconda we need to generate a kickstart file from the kickstart template and pass it to the installer as a kernel command line argument (inst.ks). Similarly the second stage of the installer (stage2) needs to fetched and it's location needs to be passed as a kernel command line argument (inst.stage2) This change also adds 'boot_anaconda' target to pxe_config.template and ipxe_config.template and renders that target correctly. The pxe configuration will automatically switch to boot_anaconda target when the boot_option is 'kickstart'. Change-Id: I3ffe5a60684cdefe51c7a0a47acc1acedbb49145 --- ironic/common/pxe_utils.py | 172 ++++++++++++++++-- ironic/drivers/modules/deploy_utils.py | 13 +- ironic/drivers/modules/ipxe_config.template | 6 + ironic/drivers/modules/pxe_base.py | 9 +- ironic/drivers/modules/pxe_config.template | 5 + ironic/tests/unit/common/test_pxe_utils.py | 139 ++++++++++++++ .../tests/unit/drivers/ipxe_config.template | 6 + .../ipxe_config_boot_from_iso.template | 6 + ...fig_boot_from_volume_extra_volume.template | 6 + ...config_boot_from_volume_multipath.template | 6 + ...boot_from_volume_no_extra_volumes.template | 6 + .../unit/drivers/ipxe_config_timeout.template | 6 + .../tests/unit/drivers/modules/test_ipxe.py | 15 +- ironic/tests/unit/drivers/modules/test_pxe.py | 68 ++++++- ironic/tests/unit/drivers/pxe_config.template | 5 + 15 files changed, 441 insertions(+), 27 deletions(-) diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index b05306a16d..b670d983de 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import os from ironic_lib import utils as ironic_utils @@ -29,6 +30,7 @@ from ironic.common import image_service as service from ironic.common import images from ironic.common import states from ironic.common import utils +from ironic.conductor import utils as manager_utils from ironic.conf import CONF from ironic.drivers.modules import boot_mode_utils from ironic.drivers.modules import deploy_utils @@ -244,6 +246,54 @@ def get_pxe_config_file_path(node_uuid, ipxe_enabled=False): return os.path.join(get_root_dir(), node_uuid, 'config') +def get_file_path_from_label(node_uuid, root_dir, label): + """Generate absolute paths to various images from their name(label) + + This method generates absolute file system path on the conductor where + various images need to be placed. For example the kickstart template, file + and stage2 squashfs.img needs to be placed in the ipxe_root_dir since they + will be transferred by anaconda ramdisk over http(s). The generated paths + will be added to the image_info dictionary as values. + + :param node_uuid: the UUID of the node + :param root_dir: Directory in which the image must be placed + :param label: Name of the image + """ + if label == 'ks_template': + return os.path.join(get_ipxe_root_dir(), node_uuid, 'ks.cfg.template') + elif label == 'ks_cfg': + return os.path.join(get_ipxe_root_dir(), node_uuid, 'ks.cfg') + elif label == 'stage2': + return os.path.join(get_ipxe_root_dir(), node_uuid, 'LiveOS', + 'squashfs.img') + else: + return os.path.join(root_dir, node_uuid, label) + + +def get_http_url_path_from_label(http_url, node_uuid, label): + """Generate http url path to various image artifacts + + This method generates http(s) urls for various image artifacts int the + webserver root. The generated urls will be added to the pxe_options dict + and used to render pxe/ipxe configuration templates. + + :param http_url: URL to access the root of the webserver + :param node_uuid: the UUID of the node + :param label: Name of the image + """ + if label == 'ks_template': + return '/'.join([http_url, node_uuid, 'ks.cfg.template']) + elif label == 'ks_cfg': + return '/'.join([http_url, node_uuid, 'ks.cfg']) + elif label == 'stage2': + # we store stage2 in http_root/node_uuid/LiveOS/squashfs.img + # Specifying http://host/node_uuid as stage2 url will make anaconda + # automatically load the squashfs.img from LiveOS directory. + return '/'.join([http_url, node_uuid]) + else: + return '/'.join([http_url, node_uuid, label]) + + def create_pxe_config(task, pxe_options, template=None, ipxe_enabled=False): """Generate PXE configuration file and MAC address links for it. @@ -642,10 +692,39 @@ def get_instance_image_info(task, ipxe_enabled=False): i_info[label] = str(iproperties[label + '_id']) node.instance_info = i_info node.save() - for label in labels: + + anaconda_labels = () + if deploy_utils.get_boot_option(node) == 'kickstart': + # stage2 - Installer stage2 squashfs image + # ks_template - Anaconda kickstart template + # ks_cfg - rendered ks_template + anaconda_labels = ('stage2', 'ks_template', 'ks_cfg') + if not (i_info.get('stage2') and i_info.get('ks_template')): + iproperties = glance_service.show( + d_info['image_source'] + )['properties'] + for label in anaconda_labels: + # ks_template is an optional property on the image + if (label == 'ks_template' + and not iproperties.get('ks_template')): + i_info[label] = CONF.anaconda.default_ks_template + elif label == 'ks_cfg': + i_info[label] = '' + elif label == 'stage2' and 'stage2_id' not in iproperties: + msg = ("stage2_id property missing on the image. " + "The anaconda deploy interface requires stage2_id " + "property to be associated with the os image. ") + raise exception.ImageUnacceptable(msg) + else: + i_info[label] = str(iproperties['stage2_id']) + + node.instance_info = i_info + node.save() + + for label in labels + anaconda_labels: image_info[label] = ( i_info[label], - os.path.join(root_dir, node.uuid, label) + get_file_path_from_label(node.uuid, root_dir, label) ) return image_info @@ -705,15 +784,18 @@ def build_instance_pxe_options(task, pxe_info, ipxe_enabled=False): node = task.node for label, option in (('kernel', 'aki_path'), - ('ramdisk', 'ari_path')): + ('ramdisk', 'ari_path'), + ('stage2', 'stage2_url'), + ('ks_template', 'ks_template_path'), + ('ks_cfg', 'ks_cfg_url')): if label in pxe_info: - if ipxe_enabled: + if ipxe_enabled or label in ('stage2', 'ks_template', 'ks_cfg'): # NOTE(pas-ha) do not use Swift TempURLs for kernel and # ramdisk of user image when boot_option is not local, # as this breaks instance reboot later when temp urls # have timed out. - pxe_opts[option] = '/'.join( - [CONF.deploy.http_url, node.uuid, label]) + pxe_opts[option] = get_http_url_path_from_label( + CONF.deploy.http_url, node.uuid, label) else: # 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. @@ -810,7 +892,8 @@ def build_service_pxe_config(task, instance_image_info, root_uuid_or_disk_id, ramdisk_boot=False, ipxe_enabled=False, - is_whole_disk_image=None): + is_whole_disk_image=None, + anaconda_boot=False): node = task.node pxe_config_path = get_pxe_config_file_path(node.uuid, ipxe_enabled=ipxe_enabled) @@ -844,7 +927,38 @@ def build_service_pxe_config(task, instance_image_info, is_whole_disk_image, deploy_utils.is_trusted_boot_requested(node), deploy_utils.is_iscsi_boot(task), ramdisk_boot, - ipxe_enabled=ipxe_enabled) + ipxe_enabled=ipxe_enabled, anaconda_boot=anaconda_boot) + + +def _build_heartbeat_url(node_uuid): + + api_version = 'v1' + heartbeat_api = '%s/heartbeat/{node_uuid}' % api_version + path = heartbeat_api.format(node_uuid=node_uuid) + return "/".join([deploy_utils.get_ironic_api_url(), path]) + + +def build_kickstart_config_options(task): + """Build the kickstart template options for a node + + This method builds the kickstart template options for a node, + given all the required parameters. + + The options should then be passed to pxe_utils.create_kickstart_config to + create the actual config files. + + :param task: A TaskManager object + :returns: A dictionary of kickstart options to be used in the kickstart + template. + """ + ks_options = {} + node = task.node + manager_utils.add_secret_token(node, pregenerated=True) + node.save() + ks_options['liveimg_url'] = node.instance_info['image_url'] + ks_options['agent_token'] = node.driver_internal_info['agent_secret_token'] + ks_options['heartbeat_url'] = _build_heartbeat_url(node.uuid) + return ks_options def get_volume_pxe_options(task): @@ -949,7 +1063,8 @@ def validate_boot_parameters_for_trusted_boot(node): def prepare_instance_pxe_config(task, image_info, iscsi_boot=False, ramdisk_boot=False, - ipxe_enabled=False): + ipxe_enabled=False, + anaconda_boot=False): """Prepares the config file for PXE boot :param task: a task from TaskManager. @@ -959,6 +1074,7 @@ def prepare_instance_pxe_config(task, image_info, :param ramdisk_boot: if the boot is to a ramdisk configuration. :param ipxe_enabled: Default false boolean to indicate if ipxe is in use by the caller. + :param anaconda_boot: if the boot is to a anaconda ramdisk configuration. :returns: None """ node = task.node @@ -978,7 +1094,7 @@ def prepare_instance_pxe_config(task, image_info, node.uuid, ipxe_enabled=ipxe_enabled) if not os.path.isfile(pxe_config_path): pxe_options = build_pxe_config_options( - task, image_info, service=ramdisk_boot, + task, image_info, service=ramdisk_boot or anaconda_boot, ipxe_enabled=ipxe_enabled) if ipxe_enabled: pxe_config_template = ( @@ -993,7 +1109,23 @@ def prepare_instance_pxe_config(task, image_info, pxe_config_path, None, boot_mode_utils.get_boot_mode(node), False, iscsi_boot=iscsi_boot, ramdisk_boot=ramdisk_boot, - ipxe_enabled=ipxe_enabled) + ipxe_enabled=ipxe_enabled, anaconda_boot=anaconda_boot) + + +def prepare_instance_kickstart_config(task, image_info, anaconda_boot=False): + """Prepare to boot anaconda ramdisk by generating kickstart file + + :param task: a task from TaskManager. + :param image_info: a dict of values of instance image + metadata to set on the configuration file. + :param anaconda_boot: if the boot is to a anaconda ramdisk configuration. + """ + if not anaconda_boot: + return + ks_options = build_kickstart_config_options(task) + kickstart_template = image_info['ks_template'][1] + ks_cfg = utils.render_template(kickstart_template, ks_options) + utils.write_to_file(image_info['ks_cfg'][1], ks_cfg) @image_cache.cleanup(priority=25) @@ -1012,14 +1144,30 @@ def cache_ramdisk_kernel(task, pxe_info, ipxe_enabled=False): """Fetch the necessary kernels and ramdisks for the instance.""" ctx = task.context node = task.node + t_pxe_info = copy.copy(pxe_info) if ipxe_enabled: path = os.path.join(get_ipxe_root_dir(), node.uuid) else: path = os.path.join(get_root_dir(), node.uuid) fileutils.ensure_tree(path) + # anconda deploy will have 'stage2' as one of the labels in pxe_info dict + if 'stage2' in pxe_info.keys(): + # stage2 will be stored in ipxe http directory. So make sure they + # exist. + fileutils.ensure_tree( + get_file_path_from_label( + node.uuid, + get_ipxe_root_dir(), + 'stage2' + ) + ) + # ks_cfg is rendered later by the driver using ks_template. It cannot + # be fetched and cached. + t_pxe_info.pop('ks_cfg') + LOG.debug("Fetching necessary kernel and ramdisk for node %s", node.uuid) - deploy_utils.fetch_images(ctx, TFTPImageCache(), list(pxe_info.values()), + deploy_utils.fetch_images(ctx, TFTPImageCache(), list(t_pxe_info.values()), CONF.force_raw_images) diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 432bddbabe..ba8ebbebd4 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -131,7 +131,8 @@ def _replace_root_uuid(path, root_uuid): def _replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot=False, iscsi_boot=False, - ramdisk_boot=False, ipxe_enabled=False): + ramdisk_boot=False, ipxe_enabled=False, + anaconda_boot=False): if is_whole_disk_image: boot_disk_type = 'boot_whole_disk' elif trusted_boot: @@ -140,6 +141,8 @@ def _replace_boot_line(path, boot_mode, is_whole_disk_image, boot_disk_type = 'boot_iscsi' elif ramdisk_boot: boot_disk_type = 'boot_ramdisk' + elif anaconda_boot: + boot_disk_type = 'boot_anaconda' else: boot_disk_type = 'boot_partition' @@ -163,7 +166,7 @@ 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, iscsi_boot=False, ramdisk_boot=False, - ipxe_enabled=False): + ipxe_enabled=False, anaconda_boot=False): """Switch a pxe config from deployment mode to service mode. :param path: path to the pxe config file in tftpboot. @@ -178,15 +181,17 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, :param ramdisk_boot: if the boot is to be to a ramdisk configuration. :param ipxe_enabled: A default False boolean value to tell the method if the caller is using iPXE. + :param anaconda_boot: if the boot is to be to an anaconda configuration. """ - if not ramdisk_boot and root_uuid_or_disk_id is not None: + if (not (ramdisk_boot or anaconda_boot) + and root_uuid_or_disk_id is not None): 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, - iscsi_boot, ramdisk_boot, ipxe_enabled) + iscsi_boot, ramdisk_boot, ipxe_enabled, anaconda_boot) def check_for_missing_params(info_dict, error_msg, param_prefix=''): diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template index 1321a2a0d7..32afea5ec6 100644 --- a/ironic/drivers/modules/ipxe_config.template +++ b/ironic/drivers/modules/ipxe_config.template @@ -31,6 +31,12 @@ kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeou initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_partition boot +:boot_anaconda +imgfree +kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.aki_path }} text {{ pxe_options.pxe_append_params|default("", true) }} inst.ks={{ pxe_options.ks_cfg_url }} inst.stage2={{ pxe_options.stage2_url }} initrd=ramdisk || goto boot_anaconda +initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_anaconda +boot + :boot_ramdisk imgfree {%- if pxe_options.boot_iso_url %} diff --git a/ironic/drivers/modules/pxe_base.py b/ironic/drivers/modules/pxe_base.py index c0b04309be..d0c3a5e4ac 100644 --- a/ironic/drivers/modules/pxe_base.py +++ b/ironic/drivers/modules/pxe_base.py @@ -234,18 +234,23 @@ class PXEBaseMixin(object): boot_option = deploy_utils.get_boot_option(node) boot_device = None instance_image_info = {} - if boot_option == "ramdisk": + if boot_option == "ramdisk" or boot_option == "kickstart": instance_image_info = pxe_utils.get_instance_image_info( task, ipxe_enabled=self.ipxe_enabled) pxe_utils.cache_ramdisk_kernel(task, instance_image_info, ipxe_enabled=self.ipxe_enabled) - if deploy_utils.is_iscsi_boot(task) or boot_option == "ramdisk": + if (deploy_utils.is_iscsi_boot(task) or boot_option == "ramdisk" + or boot_option == "kickstart"): pxe_utils.prepare_instance_pxe_config( task, instance_image_info, iscsi_boot=deploy_utils.is_iscsi_boot(task), ramdisk_boot=(boot_option == "ramdisk"), + anaconda_boot=(boot_option == "kickstart"), ipxe_enabled=self.ipxe_enabled) + pxe_utils.prepare_instance_kickstart_config( + task, instance_image_info, + anaconda_boot=(boot_option == "kickstart")) boot_device = boot_devices.PXE elif boot_option != "local": diff --git a/ironic/drivers/modules/pxe_config.template b/ironic/drivers/modules/pxe_config.template index e60be13928..46597403bf 100644 --- a/ironic/drivers/modules/pxe_config.template +++ b/ironic/drivers/modules/pxe_config.template @@ -22,3 +22,8 @@ append tboot.gz --- {{pxe_options.aki_path}} root={{ ROOT }} ro text {{ pxe_opti label boot_ramdisk kernel {{ pxe_options.aki_path }} append initrd={{ pxe_options.ari_path }} root=/dev/ram0 text {{ pxe_options.pxe_append_params|default("", true) }} {{ pxe_options.ramdisk_opts|default('', true) }} + +label boot_anaconda +kernel {{ pxe_options.aki_path }} +append initrd={{ pxe_options.ari_path }} text {{ pxe_options.pxe_append_params|default("", true) }} inst.ks={{ pxe_options.ks_cfg_url }} inst.stage2={{ pxe_options.stage2_url }} +ipappend 2 diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index ce56eb276c..c7e5d76304 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -62,6 +62,8 @@ class TestPXEUtils(db_base.DbTestCase): 'ipa-api-url': 'http://192.168.122.184:6385', 'ipxe_timeout': 0, 'ramdisk_opts': 'ramdisk_param', + 'ks_cfg_url': 'http://fake/ks.cfg', + 'stage2_url': 'http://fake/stage2' } self.ipxe_options = self.pxe_options.copy() @@ -1144,6 +1146,81 @@ class PXEInterfacesTestCase(db_base.DbTestCase): boot_opt_mock.assert_called_once_with(task.node) + @mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option', + return_value='kickstart', autospec=True) + @mock.patch.object(image_service.GlanceImageService, 'show', autospec=True) + def test_get_instance_image_info_with_kickstart_boot_option( + self, image_show_mock, boot_opt_mock): + properties = {'properties': {u'kernel_id': u'instance_kernel_uuid', + u'ramdisk_id': u'instance_ramdisk_uuid', + u'stage2_id': u'instance_stage2_id'}} + + expected_info = {'ramdisk': + ('instance_ramdisk_uuid', + os.path.join(CONF.pxe.tftp_root, + self.node.uuid, + 'ramdisk')), + 'kernel': + ('instance_kernel_uuid', + os.path.join(CONF.pxe.tftp_root, + self.node.uuid, + 'kernel')), + 'stage2': + ('instance_stage2_id', + os.path.join(CONF.deploy.http_root, + self.node.uuid, + 'LiveOS', + 'squashfs.img')), + 'ks_template': + (CONF.anaconda.default_ks_template, + os.path.join(CONF.deploy.http_root, + self.node.uuid, + 'ks.cfg.template')), + 'ks_cfg': + ('', + os.path.join(CONF.deploy.http_root, + self.node.uuid, + 'ks.cfg'))} + image_show_mock.return_value = properties + self.context.auth_token = 'fake' + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + image_info = pxe_utils.get_instance_image_info( + task, ipxe_enabled=False) + self.assertEqual(expected_info, image_info) + # In the absense of kickstart template in both instance_info and + # image default kickstart template is used + self.assertEqual(CONF.anaconda.default_ks_template, + image_info['ks_template'][0]) + calls = [mock.call(task.node), mock.call(task.node)] + boot_opt_mock.assert_has_calls(calls) + # Instance info gets presedence over kickstart template on the + # image + properties['properties'] = {'ks_template': 'glance://template_id'} + task.node.instance_info['ks_template'] = 'https://server/fake.tmpl' + image_show_mock.return_value = properties + image_info = pxe_utils.get_instance_image_info( + task, ipxe_enabled=False) + self.assertEqual('https://server/fake.tmpl', + image_info['ks_template'][0]) + + @mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option', + return_value='kickstart', autospec=True) + @mock.patch.object(image_service.GlanceImageService, 'show', autospec=True) + def test_get_instance_image_info_kickstart_stage2_missing( + self, image_show_mock, boot_opt_mock): + properties = {'properties': {u'kernel_id': u'instance_kernel_uuid', + u'ramdisk_id': u'instance_ramdisk_uuid'}} + + image_show_mock.return_value = properties + self.context.auth_token = 'fake' + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises( + exception.ImageUnacceptable, pxe_utils.get_instance_image_info, + task, ipxe_enabled=False + ) + @mock.patch.object(deploy_utils, 'fetch_images', autospec=True) def test__cache_tftp_images_master_path(self, mock_fetch_image): temp_dir = tempfile.mkdtemp() @@ -1242,6 +1319,68 @@ class PXEInterfacesTestCase(db_base.DbTestCase): self.assertFalse(mock_log.called) +@mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None) +class PXEBuildKickstartConfigOptionsTestCase(db_base.DbTestCase): + def setUp(self): + super(PXEBuildKickstartConfigOptionsTestCase, self).setUp() + n = { + 'driver': 'fake-hardware', + 'boot_interface': 'pxe', + 'instance_info': INST_INFO_DICT, + 'driver_info': DRV_INFO_DICT, + 'driver_internal_info': DRV_INTERNAL_INFO_DICT, + } + n['instance_info']['image_url'] = 'http://ironic/node/os_image.tar' + self.config_temp_dir('http_root', group='deploy') + self.node = object_utils.create_test_node(self.context, **n) + + @mock.patch.object(deploy_utils, 'get_ironic_api_url', autospec=True) + def test_build_kickstart_config_options_pxe(self, api_url_mock): + api_url_mock.return_value = 'http://ironic-api' + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + expected = {} + expected['liveimg_url'] = task.node.instance_info['image_url'] + expected['heartbeat_url'] = ( + 'http://ironic-api/v1/heartbeat/%s' % task.node.uuid + ) + ks_options = pxe_utils.build_kickstart_config_options(task) + self.assertTrue(ks_options.pop('agent_token')) + self.assertEqual(expected, ks_options) + + @mock.patch('ironic.common.utils.render_template', autospec=True) + def test_prepare_instance_kickstart_config_not_anaconda_boot(self, + render_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertFalse( + pxe_utils.prepare_instance_kickstart_config(task, {}) + ) + render_mock.assert_not_called() + + @mock.patch('ironic.common.utils.render_template', autospec=True) + @mock.patch('ironic.common.pxe_utils.build_kickstart_config_options', + autospec=True) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + def test_prepare_instance_kickstart_config(self, write_mock, + ks_options_mock, render_mock): + image_info = { + 'ks_cfg': ['', '/http_root/node_uuid/ks.cfg'], + 'ks_template': ['tmpl_id', '/http_root/node_uuid/ks.cfg.template'] + } + ks_options = {'liveimg_url': 'http://fake', 'agent_token': 'faketoken', + 'heartbeat_url': 'http://fake_hb'} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ks_options_mock.return_value = ks_options + pxe_utils.prepare_instance_kickstart_config(task, image_info, + anaconda_boot=True) + render_mock.assert_called_with(image_info['ks_template'][1], + ks_options) + write_mock.assert_called_with(image_info['ks_cfg'][1], + render_mock.return_value) + + @mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None) class PXEBuildConfigOptionsTestCase(db_base.DbTestCase): def setUp(self): diff --git a/ironic/tests/unit/drivers/ipxe_config.template b/ironic/tests/unit/drivers/ipxe_config.template index 2f1eb098a0..70f8a03f1e 100644 --- a/ironic/tests/unit/drivers/ipxe_config.template +++ b/ironic/tests/unit/drivers/ipxe_config.template @@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_anaconda +imgfree +kernel http://1.2.3.4:1234/kernel text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda +initrd http://1.2.3.4:1234/ramdisk || goto boot_anaconda +boot + :boot_ramdisk imgfree kernel http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template index 2a8e79d17b..c7133c7b61 100644 --- a/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template @@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_anaconda +imgfree +kernel http://1.2.3.4:1234/kernel text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda +initrd http://1.2.3.4:1234/ramdisk || goto boot_anaconda +boot + :boot_ramdisk imgfree sanboot http://1.2.3.4:1234/uuid/iso diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template index f2a486747a..0a872804a6 100644 --- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template @@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_anaconda +imgfree +kernel http://1.2.3.4:1234/kernel text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda +initrd http://1.2.3.4:1234/ramdisk || goto boot_anaconda +boot + :boot_ramdisk imgfree kernel http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template index f5027b3af2..571216e399 100644 --- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template @@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_anaconda +imgfree +kernel http://1.2.3.4:1234/kernel text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda +initrd http://1.2.3.4:1234/ramdisk || goto boot_anaconda +boot + :boot_ramdisk imgfree kernel http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template index 9308a5736f..6b7a4394db 100644 --- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template @@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd initrd http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_anaconda +imgfree +kernel http://1.2.3.4:1234/kernel text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda +initrd http://1.2.3.4:1234/ramdisk || goto boot_anaconda +boot + :boot_ramdisk imgfree kernel http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk diff --git a/ironic/tests/unit/drivers/ipxe_config_timeout.template b/ironic/tests/unit/drivers/ipxe_config_timeout.template index 90b0b43011..2458f010b3 100644 --- a/ironic/tests/unit/drivers/ipxe_config_timeout.template +++ b/ironic/tests/unit/drivers/ipxe_config_timeout.template @@ -31,6 +31,12 @@ kernel --timeout 120 http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_par initrd --timeout 120 http://1.2.3.4:1234/ramdisk || goto boot_partition boot +:boot_anaconda +imgfree +kernel --timeout 120 http://1.2.3.4:1234/kernel text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda +initrd --timeout 120 http://1.2.3.4:1234/ramdisk || goto boot_anaconda +boot + :boot_ramdisk imgfree kernel --timeout 120 http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk diff --git a/ironic/tests/unit/drivers/modules/test_ipxe.py b/ironic/tests/unit/drivers/modules/test_ipxe.py index 5229cc2500..e576922687 100644 --- a/ironic/tests/unit/drivers/modules/test_ipxe.py +++ b/ironic/tests/unit/drivers/modules/test_ipxe.py @@ -600,7 +600,8 @@ class iPXEBootTestCase(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, False, False, ipxe_enabled=True) + 'bios', False, False, False, False, ipxe_enabled=True, + anaconda_boot=False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -649,7 +650,8 @@ class iPXEBootTestCase(db_base.DbTestCase): ipxe_enabled=True) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False, False, False, ipxe_enabled=True) + 'bios', False, False, False, False, ipxe_enabled=True, + anaconda_boot=False) self.assertFalse(set_boot_device_mock.called) @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) @@ -766,7 +768,8 @@ class iPXEBootTestCase(db_base.DbTestCase): ipxe_enabled=True) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, None, boot_modes.LEGACY_BIOS, False, - ipxe_enabled=True, iscsi_boot=True, ramdisk_boot=False) + ipxe_enabled=True, iscsi_boot=True, ramdisk_boot=False, + anaconda_boot=False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -812,7 +815,8 @@ class iPXEBootTestCase(db_base.DbTestCase): ipxe_enabled=True) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, None, boot_modes.LEGACY_BIOS, False, - ipxe_enabled=True, iscsi_boot=False, ramdisk_boot=True) + ipxe_enabled=True, iscsi_boot=False, ramdisk_boot=True, + anaconda_boot=False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -880,7 +884,8 @@ class iPXEBootTestCase(db_base.DbTestCase): persistent=True) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', True, False, False, False, ipxe_enabled=True) + 'bios', True, False, False, False, ipxe_enabled=True, + anaconda_boot=False) # No clean up self.assertFalse(clean_up_pxe_config_mock.called) # No netboot configuration beyond the PXE files diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index d9fdb63ad6..a204f6954b 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -74,6 +74,7 @@ class PXEBootTestCase(db_base.DbTestCase): group='anaconda') instance_info = INST_INFO_DICT instance_info['deploy_key'] = 'fake-56789' + instance_info['image_url'] = 'http://fakeserver/os.tar.gz' self.config(enabled_boot_interfaces=[self.boot_interface, 'ipxe', 'fake']) @@ -527,7 +528,8 @@ 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, False, False, ipxe_enabled=False) + 'bios', False, False, False, False, ipxe_enabled=False, + anaconda_boot=False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -575,7 +577,8 @@ class PXEBootTestCase(db_base.DbTestCase): ipxe_enabled=False) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False, False, False, ipxe_enabled=False) + 'bios', False, False, False, False, ipxe_enabled=False, + anaconda_boot=False) self.assertFalse(set_boot_device_mock.called) @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) @@ -727,7 +730,7 @@ class PXEBootTestCase(db_base.DbTestCase): switch_pxe_config_mock.assert_called_once_with( pxe_config_path, None, 'bios', False, ipxe_enabled=False, iscsi_boot=False, - ramdisk_boot=True) + ramdisk_boot=True, anaconda_boot=False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -740,6 +743,63 @@ class PXEBootTestCase(db_base.DbTestCase): def test_prepare_instance_ramdisk_pxe_conf_exists(self): self._test_prepare_instance_ramdisk(config_file_exits=False) + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) + @mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True) + @mock.patch.object(pxe_utils, 'cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe_utils, 'get_instance_image_info', autospec=True) + @mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option', + return_value='kickstart', autospec=True) + @mock.patch('ironic.drivers.modules.deploy_utils.get_ironic_api_url', + return_value='http://fakeserver/api', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + def test_prepare_instance_kickstart( + self, write_file_mock, render_mock, api_url_mock, boot_opt_mock, + get_image_info_mock, cache_mock, dhcp_factory_mock, + create_pxe_config_mock, switch_pxe_config_mock, + set_boot_device_mock): + image_info = {'kernel': ['ins_kernel_id', '/path/to/kernel'], + 'ramdisk': ['ins_ramdisk_id', '/path/to/ramdisk'], + 'stage2': ['ins_stage2_id', '/path/to/stage2'], + 'ks_cfg': ['', '/path/to/ks.cfg'], + 'ks_template': ['template_id', '/path/to/ks_template']} + get_image_info_mock.return_value = image_info + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + self.node.provision_state = states.DEPLOYING + self.config(http_url='http://fake_url', group='deploy') + with task_manager.acquire(self.context, self.node.uuid) as task: + dhcp_opts = pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False) + dhcp_opts += pxe_utils.dhcp_options_for_instance( + task, ipxe_enabled=False, ip_version=6) + pxe_config_path = pxe_utils.get_pxe_config_file_path( + task.node.uuid) + + task.driver.boot.prepare_instance(task) + + get_image_info_mock.assert_called_once_with(task, + ipxe_enabled=False) + cache_mock.assert_called_once_with( + task, image_info, False) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + render_mock.assert_called() + write_file_mock.assert_called_with( + '/path/to/ks.cfg', render_mock.return_value + ) + create_pxe_config_mock.assert_called_once_with( + task, mock.ANY, CONF.pxe.pxe_config_template, + ipxe_enabled=False) + switch_pxe_config_mock.assert_called_once_with( + pxe_config_path, None, + 'bios', False, ipxe_enabled=False, iscsi_boot=False, + ramdisk_boot=False, anaconda_boot=True) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.PXE, + persistent=True) + @mock.patch.object(boot_mode_utils, 'deconfigure_secure_boot_if_needed', autospec=True) @mock.patch.object(pxe_utils, 'clean_up_pxe_env', autospec=True) @@ -826,7 +886,7 @@ class PXERamdiskDeployTestCase(db_base.DbTestCase): switch_pxe_config_mock.assert_called_once_with( pxe_config_path, None, 'bios', False, ipxe_enabled=False, iscsi_boot=False, - ramdisk_boot=True) + ramdisk_boot=True, anaconda_boot=False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) diff --git a/ironic/tests/unit/drivers/pxe_config.template b/ironic/tests/unit/drivers/pxe_config.template index a94a816aec..b3cfa7ea07 100644 --- a/ironic/tests/unit/drivers/pxe_config.template +++ b/ironic/tests/unit/drivers/pxe_config.template @@ -22,3 +22,8 @@ append tboot.gz --- /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel root={ label boot_ramdisk kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk root=/dev/ram0 text test_param ramdisk_param + +label boot_anaconda +kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel +append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 +ipappend 2