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:
parent
e78f123ff8
commit
33bb2c248a
@ -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
|
||||
------------------
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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.
|
Loading…
Reference in New Issue
Block a user