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. 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 Deployment Process
------------------ ------------------

View File

@ -698,17 +698,25 @@ def get_instance_image_info(task, ipxe_enabled=False):
anaconda_labels = () anaconda_labels = ()
if deploy_utils.get_boot_option(node) == 'kickstart': if deploy_utils.get_boot_option(node) == 'kickstart':
isap = node.driver_internal_info.get('is_source_a_path')
# stage2: installer stage2 squashfs image # stage2: installer stage2 squashfs image
# ks_template: anaconda kickstart template # ks_template: anaconda kickstart template
# ks_cfg - rendered ks_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 # NOTE(rloo): We save stage2 & ks_template values in case they
# are changed by the user after we start using them and to # are changed by the user after we start using them and to
# prevent re-computing them again. # prevent re-computing them again.
if not node.driver_internal_info.get('stage2'): if not node.driver_internal_info.get('stage2'):
if i_info.get('stage2'): if i_info.get('stage2'):
node.set_driver_internal_info('stage2', i_info['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() _get_image_properties()
if 'stage2_id' not in image_properties: if 'stage2_id' not in image_properties:
msg = (_("'stage2_id' is missing from the properties of " msg = (_("'stage2_id' is missing from the properties of "
@ -720,19 +728,27 @@ def get_instance_image_info(task, ipxe_enabled=False):
else: else:
node.set_driver_internal_info( node.set_driver_internal_info(
'stage2', str(image_properties['stage2_id'])) 'stage2', str(image_properties['stage2_id']))
if i_info.get('ks_template'): # NOTE(TheJulia): A kickstart template is entirely independent
node.set_driver_internal_info('ks_template', # of the stage2 ramdisk. In the end, it was the configuration which
i_info['ks_template']) # 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: else:
_get_image_properties() node.set_driver_internal_info(
# ks_template is an optional property on the image 'ks_template', str(image_properties['ks_template']))
if 'ks_template' not in image_properties: node.save()
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()
for label in labels + anaconda_labels: for label in labels + anaconda_labels:
image_info[label] = ( 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): def build_instance_pxe_options(task, pxe_info, ipxe_enabled=False):
pxe_opts = {} pxe_opts = {}
node = task.node node = task.node
isap = node.driver_internal_info.get('is_source_a_path')
for label, option in (('kernel', 'aki_path'), for label, option in (('kernel', 'aki_path'),
('ramdisk', 'ari_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], pxe_opts[option] = os.path.relpath(pxe_info[label][1],
CONF.pxe.tftp_root) 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('aki_path', 'no_kernel')
pxe_opts.setdefault('ari_path', 'no_ramdisk') pxe_opts.setdefault('ari_path', 'no_ramdisk')

View File

@ -33,7 +33,7 @@ boot
:boot_anaconda :boot_anaconda
imgfree 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 initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_anaconda
boot boot

View File

@ -133,6 +133,17 @@ class TestPXEUtils(db_base.DbTestCase):
'ramdisk_kernel_arguments': 'ramdisk_params' '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) self.node = object_utils.create_test_node(self.context)
def test_default_pxe_config(self): def test_default_pxe_config(self):
@ -315,6 +326,27 @@ class TestPXEUtils(db_base.DbTestCase):
expected_template = f.read().rstrip() expected_template = f.read().rstrip()
self.assertEqual(str(expected_template), rendered_template) 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): def test_default_grub_config(self):
pxe_opts = self.pxe_options pxe_opts = self.pxe_options
pxe_opts['boot_mode'] = 'uefi' pxe_opts['boot_mode'] = 'uefi'
@ -1375,6 +1407,62 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
self.assertEqual('https://server/fake.tmpl', self.assertEqual('https://server/fake.tmpl',
image_info['ks_template'][0]) 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', @mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option',
return_value='kickstart', autospec=True) return_value='kickstart', autospec=True)
@mock.patch.object(image_service.GlanceImageService, 'show', 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.