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