diff options
author | Julia Kreger <juliaashleykreger@gmail.com> | 2022-03-29 18:32:36 -0700 |
---|---|---|
committer | Julia Kreger <juliaashleykreger@gmail.com> | 2022-07-20 06:50:03 -0700 |
commit | 33bb2c248a20d6a2a0af570655124cbc86d58b6a (patch) | |
tree | e00370d4149feeebd9bf60bba7c3bcb7f0abb9b1 /ironic | |
parent | e78f123ff8e3a4fcd5e3e596b526eb5eb39a34a9 (diff) | |
download | ironic-33bb2c248a20d6a2a0af570655124cbc86d58b6a.tar.gz |
Do not require stage2 for anaconda with standalone
The use of the anaconda deployment interface can be
confusing when using a standalone deployment model.
Specifically this is because the anaconda deployment
interface was primarily modeled for usage with glance
and the inherent configuration of a fully integrated
OpenStack deployment. The additional prameters are
confusing, so this also (hopefully) provides clarity
into use and options.
Change-Id: I748fd86901bc05d3d003626b5e14e655b7905215
Diffstat (limited to 'ironic')
-rw-r--r-- | ironic/common/pxe_utils.py | 55 | ||||
-rw-r--r-- | ironic/drivers/modules/ipxe_config.template | 2 | ||||
-rw-r--r-- | ironic/tests/unit/common/test_pxe_utils.py | 88 | ||||
-rw-r--r-- | ironic/tests/unit/drivers/ipxe_config_boot_from_anaconda.template | 47 |
4 files changed, 177 insertions, 15 deletions
diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index b0f1d906f..64cf4608f 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -698,17 +698,25 @@ def get_instance_image_info(task, ipxe_enabled=False): anaconda_labels = () if deploy_utils.get_boot_option(node) == 'kickstart': + isap = node.driver_internal_info.get('is_source_a_path') # stage2: installer stage2 squashfs image # ks_template: anaconda kickstart template # ks_cfg - rendered ks_template - anaconda_labels = ('stage2', 'ks_template', 'ks_cfg') + if not isap: + anaconda_labels = ('stage2', 'ks_template', 'ks_cfg') + else: + # When a path is used, a stage2 ramdisk can be determiend + # automatically by anaconda, so it is not an explicit + # requirement. + anaconda_labels = ('ks_template', 'ks_cfg') # NOTE(rloo): We save stage2 & ks_template values in case they # are changed by the user after we start using them and to # prevent re-computing them again. if not node.driver_internal_info.get('stage2'): if i_info.get('stage2'): node.set_driver_internal_info('stage2', i_info['stage2']) - else: + elif not isap: + # If the source is not a path, then we need a stage2 ramdisk. _get_image_properties() if 'stage2_id' not in image_properties: msg = (_("'stage2_id' is missing from the properties of " @@ -720,19 +728,27 @@ def get_instance_image_info(task, ipxe_enabled=False): else: node.set_driver_internal_info( 'stage2', str(image_properties['stage2_id'])) - if i_info.get('ks_template'): - node.set_driver_internal_info('ks_template', - i_info['ks_template']) + # NOTE(TheJulia): A kickstart template is entirely independent + # of the stage2 ramdisk. In the end, it was the configuration which + # told anaconda how to execute. + if i_info.get('ks_template'): + # If the value is set, we always overwrite it, in the event + # a rebuild is occuring or something along those lines. + node.set_driver_internal_info('ks_template', + i_info['ks_template']) + else: + _get_image_properties() + # ks_template is an optional property on the image + if 'ks_template' not in image_properties: + # If not defined, default to the overall system default + # kickstart template, as opposed to a user supplied + # template. + node.set_driver_internal_info( + 'ks_template', CONF.anaconda.default_ks_template) else: - _get_image_properties() - # ks_template is an optional property on the image - if 'ks_template' not in image_properties: - node.set_driver_internal_info( - 'ks_template', CONF.anaconda.default_ks_template) - else: - node.set_driver_internal_info( - 'ks_template', str(image_properties['ks_template'])) - node.save() + node.set_driver_internal_info( + 'ks_template', str(image_properties['ks_template'])) + node.save() for label in labels + anaconda_labels: image_info[label] = ( @@ -800,6 +816,7 @@ def build_deploy_pxe_options(task, pxe_info, mode='deploy', def build_instance_pxe_options(task, pxe_info, ipxe_enabled=False): pxe_opts = {} node = task.node + isap = node.driver_internal_info.get('is_source_a_path') for label, option in (('kernel', 'aki_path'), ('ramdisk', 'ari_path'), @@ -822,6 +839,16 @@ def build_instance_pxe_options(task, pxe_info, ipxe_enabled=False): pxe_opts[option] = os.path.relpath(pxe_info[label][1], CONF.pxe.tftp_root) + # NOTE(TheJulia): This is basically anaconda specific, but who knows + # one day! Copy image_source to repo_url if it is a URL to a directory + # path, and an explicit stage2 URL is not defined as .treeinfo is totally + # a thing and anaconda's dracut element knows the secrets of how to + # get and use the treeinfo file. And yes, this is a hidden file. :\ + # example: + # http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/.treeinfo + if isap and 'stage2_url' not in pxe_opts: + pxe_opts['repo_url'] = node.instance_info.get('image_source') + pxe_opts.setdefault('aki_path', 'no_kernel') pxe_opts.setdefault('ari_path', 'no_ramdisk') diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template index 32afea5ec..bca63c982 100644 --- a/ironic/drivers/modules/ipxe_config.template +++ b/ironic/drivers/modules/ipxe_config.template @@ -33,7 +33,7 @@ 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 +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 }} {% if pxe_options.repo_url %}inst.repo={{ pxe_options.repo_url }}{% else %}inst.stage2={{ pxe_options.stage2_url }}{% endif %} 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 diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index f38e7127a..beedb6f78 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -133,6 +133,17 @@ class TestPXEUtils(db_base.DbTestCase): 'ramdisk_kernel_arguments': 'ramdisk_params' }) + self.ipxe_kickstart_deploy = self.pxe_options.copy() + self.ipxe_kickstart_deploy.update({ + 'deployment_aki_path': 'http://1.2.3.4:1234/deploy_kernel', + 'deployment_ari_path': 'http://1.2.3.4:1234/deploy_ramdisk', + 'aki_path': 'http://1.2.3.4:1234/kernel', + 'ari_path': 'http://1.2.3.4:1234/ramdisk', + 'initrd_filename': 'deploy_ramdisk', + 'repo_url': 'http://1.2.3.4/path/to/os/', + }) + self.ipxe_kickstart_deploy.pop('stage2_url') + self.node = object_utils.create_test_node(self.context) def test_default_pxe_config(self): @@ -315,6 +326,27 @@ class TestPXEUtils(db_base.DbTestCase): expected_template = f.read().rstrip() self.assertEqual(str(expected_template), rendered_template) + def test_default_ipxe_boot_from_anaconda(self): + self.config( + pxe_config_template='ironic/drivers/modules/ipxe_config.template', + group='pxe' + ) + self.config(http_url='http://1.2.3.4:1234', group='deploy') + + pxe_options = self.ipxe_kickstart_deploy + + rendered_template = utils.render_template( + CONF.pxe.ipxe_config_template, + {'pxe_options': pxe_options, + 'ROOT': '{{ ROOT }}'}, + ) + + templ_file = 'ironic/tests/unit/drivers/' \ + 'ipxe_config_boot_from_anaconda.template' + with open(templ_file) as f: + expected_template = f.read().rstrip() + self.assertEqual(str(expected_template), rendered_template) + def test_default_grub_config(self): pxe_opts = self.pxe_options pxe_opts['boot_mode'] = 'uefi' @@ -1378,6 +1410,62 @@ class PXEInterfacesTestCase(db_base.DbTestCase): @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_url( + self, image_show_mock, boot_opt_mock): + properties = {'properties': {u'kernel_id': u'instance_kernel_uuid', + u'ramdisk_id': u'instance_ramdisk_uuid', + u'image_source': u'http://path/to/os/'}} + + 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')), + '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: + dii = task.node.driver_internal_info + dii['is_source_a_path'] = True + task.node.driver_internal_info = dii + task.node.save() + 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', diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_anaconda.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_anaconda.template new file mode 100644 index 000000000..7963b3883 --- /dev/null +++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_anaconda.template @@ -0,0 +1,47 @@ +#!ipxe + +set attempts:int32 10 +set i:int32 0 + +goto deploy + +:deploy +imgfree +kernel http://1.2.3.4:1234/deploy_kernel selinux=0 troubleshoot=0 text test_param BOOTIF=${mac} initrd=deploy_ramdisk || goto retry + +initrd http://1.2.3.4:1234/deploy_ramdisk || goto retry +boot + +:retry +iseq ${i} ${attempts} && goto fail || +inc i +echo No response, retrying in ${i} seconds. +sleep ${i} +goto deploy + +:fail +echo Failed to get a response after ${attempts} attempts +echo Powering off in 30 seconds. +sleep 30 +poweroff + +:boot_partition +imgfree +kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramdisk || goto boot_partition +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.repo=http://1.2.3.4/path/to/os/ 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 +initrd http://1.2.3.4:1234/ramdisk || goto boot_ramdisk +boot + +:boot_whole_disk +sanboot --no-describe |