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
This commit is contained in:
Julia Kreger 2022-03-29 18:32:36 -07:00
parent e78f123ff8
commit 33bb2c248a
6 changed files with 217 additions and 15 deletions

View File

@ -218,6 +218,35 @@ collects the files, and stages them appropriately.
At this point, you should be able to request the baremetal node to deploy.
Standalone using a repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Anaconda supports a concept of passing a repository as opposed to a dedicated
URL path which has a ``.treeinfo`` file, which tells the initial boot scripts
where to get various dependencies, such as what would be used as the anaconda
``stage2`` ramdisk. Unfortunately, this functionality is not well documented.
An example ``.treeinfo`` file can be found at
http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/.treeinfo.
.. note::
In the context of the ``.treeinfo`` file and the related folder structure
for a deployment utilizing the ``anaconda`` deployment interface,
``images/install.img`` file represents a ``stage2`` ramdisk.
In the context of one wishing to deploy Centos Stream-9, the following may
be useful.
.. code-block:: shell
baremetal node set <node> \
--instance_info image_source=http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/ \
--instance_info kernel=http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/pxeboot/vmlinuz \
--instance_info ramdisk=http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/pxeboot/initrd.img
Once set, a kickstart template can be provided via an ``instance_info``
parameter, and the node deployed.
Deployment Process
------------------

View File

@ -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')

View File

@ -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

View File

@ -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'
@ -1375,6 +1407,62 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
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_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)

View File

@ -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

View File

@ -0,0 +1,11 @@
---
fixes:
- |
Anaconda supports the ability to explicitly pass a URL instead
of a ``stage2`` ramdisk parameter. This has resulted in confusion
in use of the ``anaconda`` deployment interface, as a ``stage2``
ramdisk is typically not used, but made sense with Glance images in
a fully integrated OpenStack deployment. Now a URL to a path can be
supplied to the ``anaconda`` deployment interface to simplify the
interaction and use, and a redundant ``stage2`` parameter is no longer
required.