summaryrefslogtreecommitdiff
path: root/ironic
diff options
context:
space:
mode:
authorJulia Kreger <juliaashleykreger@gmail.com>2022-03-29 18:32:36 -0700
committerJulia Kreger <juliaashleykreger@gmail.com>2022-07-20 06:50:03 -0700
commit33bb2c248a20d6a2a0af570655124cbc86d58b6a (patch)
treee00370d4149feeebd9bf60bba7c3bcb7f0abb9b1 /ironic
parente78f123ff8e3a4fcd5e3e596b526eb5eb39a34a9 (diff)
downloadironic-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.py55
-rw-r--r--ironic/drivers/modules/ipxe_config.template2
-rw-r--r--ironic/tests/unit/common/test_pxe_utils.py88
-rw-r--r--ironic/tests/unit/drivers/ipxe_config_boot_from_anaconda.template47
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