Add anaconda support in the pxe boot driver

To prepare for booting anaconda we need to generate a kickstart file
from the kickstart template and pass it to the installer as a kernel
command line argument (inst.ks). Similarly the second stage of the
installer (stage2) needs to fetched and it's location needs to be
passed as a kernel command line argument (inst.stage2)

This change also adds 'boot_anaconda' target to pxe_config.template
and ipxe_config.template and renders that target correctly. The pxe
configuration will automatically switch to boot_anaconda target when
the boot_option is 'kickstart'.

Change-Id: I3ffe5a60684cdefe51c7a0a47acc1acedbb49145
This commit is contained in:
Arun S A G 2021-02-16 02:16:32 -08:00
parent 709562731c
commit 880bd639f3
15 changed files with 441 additions and 27 deletions

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import os
from ironic_lib import utils as ironic_utils
@ -29,6 +30,7 @@ from ironic.common import image_service as service
from ironic.common import images
from ironic.common import states
from ironic.common import utils
from ironic.conductor import utils as manager_utils
from ironic.conf import CONF
from ironic.drivers.modules import boot_mode_utils
from ironic.drivers.modules import deploy_utils
@ -244,6 +246,54 @@ def get_pxe_config_file_path(node_uuid, ipxe_enabled=False):
return os.path.join(get_root_dir(), node_uuid, 'config')
def get_file_path_from_label(node_uuid, root_dir, label):
"""Generate absolute paths to various images from their name(label)
This method generates absolute file system path on the conductor where
various images need to be placed. For example the kickstart template, file
and stage2 squashfs.img needs to be placed in the ipxe_root_dir since they
will be transferred by anaconda ramdisk over http(s). The generated paths
will be added to the image_info dictionary as values.
:param node_uuid: the UUID of the node
:param root_dir: Directory in which the image must be placed
:param label: Name of the image
"""
if label == 'ks_template':
return os.path.join(get_ipxe_root_dir(), node_uuid, 'ks.cfg.template')
elif label == 'ks_cfg':
return os.path.join(get_ipxe_root_dir(), node_uuid, 'ks.cfg')
elif label == 'stage2':
return os.path.join(get_ipxe_root_dir(), node_uuid, 'LiveOS',
'squashfs.img')
else:
return os.path.join(root_dir, node_uuid, label)
def get_http_url_path_from_label(http_url, node_uuid, label):
"""Generate http url path to various image artifacts
This method generates http(s) urls for various image artifacts int the
webserver root. The generated urls will be added to the pxe_options dict
and used to render pxe/ipxe configuration templates.
:param http_url: URL to access the root of the webserver
:param node_uuid: the UUID of the node
:param label: Name of the image
"""
if label == 'ks_template':
return '/'.join([http_url, node_uuid, 'ks.cfg.template'])
elif label == 'ks_cfg':
return '/'.join([http_url, node_uuid, 'ks.cfg'])
elif label == 'stage2':
# we store stage2 in http_root/node_uuid/LiveOS/squashfs.img
# Specifying http://host/node_uuid as stage2 url will make anaconda
# automatically load the squashfs.img from LiveOS directory.
return '/'.join([http_url, node_uuid])
else:
return '/'.join([http_url, node_uuid, label])
def create_pxe_config(task, pxe_options, template=None, ipxe_enabled=False):
"""Generate PXE configuration file and MAC address links for it.
@ -642,10 +692,39 @@ def get_instance_image_info(task, ipxe_enabled=False):
i_info[label] = str(iproperties[label + '_id'])
node.instance_info = i_info
node.save()
for label in labels:
anaconda_labels = ()
if deploy_utils.get_boot_option(node) == 'kickstart':
# stage2 - Installer stage2 squashfs image
# ks_template - Anaconda kickstart template
# ks_cfg - rendered ks_template
anaconda_labels = ('stage2', 'ks_template', 'ks_cfg')
if not (i_info.get('stage2') and i_info.get('ks_template')):
iproperties = glance_service.show(
d_info['image_source']
)['properties']
for label in anaconda_labels:
# ks_template is an optional property on the image
if (label == 'ks_template'
and not iproperties.get('ks_template')):
i_info[label] = CONF.anaconda.default_ks_template
elif label == 'ks_cfg':
i_info[label] = ''
elif label == 'stage2' and 'stage2_id' not in iproperties:
msg = ("stage2_id property missing on the image. "
"The anaconda deploy interface requires stage2_id "
"property to be associated with the os image. ")
raise exception.ImageUnacceptable(msg)
else:
i_info[label] = str(iproperties['stage2_id'])
node.instance_info = i_info
node.save()
for label in labels + anaconda_labels:
image_info[label] = (
i_info[label],
os.path.join(root_dir, node.uuid, label)
get_file_path_from_label(node.uuid, root_dir, label)
)
return image_info
@ -705,15 +784,18 @@ def build_instance_pxe_options(task, pxe_info, ipxe_enabled=False):
node = task.node
for label, option in (('kernel', 'aki_path'),
('ramdisk', 'ari_path')):
('ramdisk', 'ari_path'),
('stage2', 'stage2_url'),
('ks_template', 'ks_template_path'),
('ks_cfg', 'ks_cfg_url')):
if label in pxe_info:
if ipxe_enabled:
if ipxe_enabled or label in ('stage2', 'ks_template', 'ks_cfg'):
# NOTE(pas-ha) do not use Swift TempURLs for kernel and
# ramdisk of user image when boot_option is not local,
# as this breaks instance reboot later when temp urls
# have timed out.
pxe_opts[option] = '/'.join(
[CONF.deploy.http_url, node.uuid, label])
pxe_opts[option] = get_http_url_path_from_label(
CONF.deploy.http_url, node.uuid, label)
else:
# It is possible that we don't have kernel/ramdisk or even
# image_source to determine if it's a whole disk image or not.
@ -810,7 +892,8 @@ def build_service_pxe_config(task, instance_image_info,
root_uuid_or_disk_id,
ramdisk_boot=False,
ipxe_enabled=False,
is_whole_disk_image=None):
is_whole_disk_image=None,
anaconda_boot=False):
node = task.node
pxe_config_path = get_pxe_config_file_path(node.uuid,
ipxe_enabled=ipxe_enabled)
@ -844,7 +927,38 @@ def build_service_pxe_config(task, instance_image_info,
is_whole_disk_image,
deploy_utils.is_trusted_boot_requested(node),
deploy_utils.is_iscsi_boot(task), ramdisk_boot,
ipxe_enabled=ipxe_enabled)
ipxe_enabled=ipxe_enabled, anaconda_boot=anaconda_boot)
def _build_heartbeat_url(node_uuid):
api_version = 'v1'
heartbeat_api = '%s/heartbeat/{node_uuid}' % api_version
path = heartbeat_api.format(node_uuid=node_uuid)
return "/".join([deploy_utils.get_ironic_api_url(), path])
def build_kickstart_config_options(task):
"""Build the kickstart template options for a node
This method builds the kickstart template options for a node,
given all the required parameters.
The options should then be passed to pxe_utils.create_kickstart_config to
create the actual config files.
:param task: A TaskManager object
:returns: A dictionary of kickstart options to be used in the kickstart
template.
"""
ks_options = {}
node = task.node
manager_utils.add_secret_token(node, pregenerated=True)
node.save()
ks_options['liveimg_url'] = node.instance_info['image_url']
ks_options['agent_token'] = node.driver_internal_info['agent_secret_token']
ks_options['heartbeat_url'] = _build_heartbeat_url(node.uuid)
return ks_options
def get_volume_pxe_options(task):
@ -949,7 +1063,8 @@ def validate_boot_parameters_for_trusted_boot(node):
def prepare_instance_pxe_config(task, image_info,
iscsi_boot=False,
ramdisk_boot=False,
ipxe_enabled=False):
ipxe_enabled=False,
anaconda_boot=False):
"""Prepares the config file for PXE boot
:param task: a task from TaskManager.
@ -959,6 +1074,7 @@ def prepare_instance_pxe_config(task, image_info,
:param ramdisk_boot: if the boot is to a ramdisk configuration.
:param ipxe_enabled: Default false boolean to indicate if ipxe
is in use by the caller.
:param anaconda_boot: if the boot is to a anaconda ramdisk configuration.
:returns: None
"""
node = task.node
@ -978,7 +1094,7 @@ def prepare_instance_pxe_config(task, image_info,
node.uuid, ipxe_enabled=ipxe_enabled)
if not os.path.isfile(pxe_config_path):
pxe_options = build_pxe_config_options(
task, image_info, service=ramdisk_boot,
task, image_info, service=ramdisk_boot or anaconda_boot,
ipxe_enabled=ipxe_enabled)
if ipxe_enabled:
pxe_config_template = (
@ -993,7 +1109,23 @@ def prepare_instance_pxe_config(task, image_info,
pxe_config_path, None,
boot_mode_utils.get_boot_mode(node), False,
iscsi_boot=iscsi_boot, ramdisk_boot=ramdisk_boot,
ipxe_enabled=ipxe_enabled)
ipxe_enabled=ipxe_enabled, anaconda_boot=anaconda_boot)
def prepare_instance_kickstart_config(task, image_info, anaconda_boot=False):
"""Prepare to boot anaconda ramdisk by generating kickstart file
:param task: a task from TaskManager.
:param image_info: a dict of values of instance image
metadata to set on the configuration file.
:param anaconda_boot: if the boot is to a anaconda ramdisk configuration.
"""
if not anaconda_boot:
return
ks_options = build_kickstart_config_options(task)
kickstart_template = image_info['ks_template'][1]
ks_cfg = utils.render_template(kickstart_template, ks_options)
utils.write_to_file(image_info['ks_cfg'][1], ks_cfg)
@image_cache.cleanup(priority=25)
@ -1012,14 +1144,30 @@ def cache_ramdisk_kernel(task, pxe_info, ipxe_enabled=False):
"""Fetch the necessary kernels and ramdisks for the instance."""
ctx = task.context
node = task.node
t_pxe_info = copy.copy(pxe_info)
if ipxe_enabled:
path = os.path.join(get_ipxe_root_dir(), node.uuid)
else:
path = os.path.join(get_root_dir(), node.uuid)
fileutils.ensure_tree(path)
# anconda deploy will have 'stage2' as one of the labels in pxe_info dict
if 'stage2' in pxe_info.keys():
# stage2 will be stored in ipxe http directory. So make sure they
# exist.
fileutils.ensure_tree(
get_file_path_from_label(
node.uuid,
get_ipxe_root_dir(),
'stage2'
)
)
# ks_cfg is rendered later by the driver using ks_template. It cannot
# be fetched and cached.
t_pxe_info.pop('ks_cfg')
LOG.debug("Fetching necessary kernel and ramdisk for node %s",
node.uuid)
deploy_utils.fetch_images(ctx, TFTPImageCache(), list(pxe_info.values()),
deploy_utils.fetch_images(ctx, TFTPImageCache(), list(t_pxe_info.values()),
CONF.force_raw_images)

View File

@ -131,7 +131,8 @@ def _replace_root_uuid(path, root_uuid):
def _replace_boot_line(path, boot_mode, is_whole_disk_image,
trusted_boot=False, iscsi_boot=False,
ramdisk_boot=False, ipxe_enabled=False):
ramdisk_boot=False, ipxe_enabled=False,
anaconda_boot=False):
if is_whole_disk_image:
boot_disk_type = 'boot_whole_disk'
elif trusted_boot:
@ -140,6 +141,8 @@ def _replace_boot_line(path, boot_mode, is_whole_disk_image,
boot_disk_type = 'boot_iscsi'
elif ramdisk_boot:
boot_disk_type = 'boot_ramdisk'
elif anaconda_boot:
boot_disk_type = 'boot_anaconda'
else:
boot_disk_type = 'boot_partition'
@ -163,7 +166,7 @@ def _replace_disk_identifier(path, disk_identifier):
def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode,
is_whole_disk_image, trusted_boot=False,
iscsi_boot=False, ramdisk_boot=False,
ipxe_enabled=False):
ipxe_enabled=False, anaconda_boot=False):
"""Switch a pxe config from deployment mode to service mode.
:param path: path to the pxe config file in tftpboot.
@ -178,15 +181,17 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode,
:param ramdisk_boot: if the boot is to be to a ramdisk configuration.
:param ipxe_enabled: A default False boolean value to tell the method
if the caller is using iPXE.
:param anaconda_boot: if the boot is to be to an anaconda configuration.
"""
if not ramdisk_boot and root_uuid_or_disk_id is not None:
if (not (ramdisk_boot or anaconda_boot)
and root_uuid_or_disk_id is not None):
if not is_whole_disk_image:
_replace_root_uuid(path, root_uuid_or_disk_id)
else:
_replace_disk_identifier(path, root_uuid_or_disk_id)
_replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot,
iscsi_boot, ramdisk_boot, ipxe_enabled)
iscsi_boot, ramdisk_boot, ipxe_enabled, anaconda_boot)
def check_for_missing_params(info_dict, error_msg, param_prefix=''):

View File

@ -31,6 +31,12 @@ kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeou
initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_partition
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
initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ pxe_options.ari_path }} || goto boot_anaconda
boot
:boot_ramdisk
imgfree
{%- if pxe_options.boot_iso_url %}

View File

@ -234,18 +234,23 @@ class PXEBaseMixin(object):
boot_option = deploy_utils.get_boot_option(node)
boot_device = None
instance_image_info = {}
if boot_option == "ramdisk":
if boot_option == "ramdisk" or boot_option == "kickstart":
instance_image_info = pxe_utils.get_instance_image_info(
task, ipxe_enabled=self.ipxe_enabled)
pxe_utils.cache_ramdisk_kernel(task, instance_image_info,
ipxe_enabled=self.ipxe_enabled)
if deploy_utils.is_iscsi_boot(task) or boot_option == "ramdisk":
if (deploy_utils.is_iscsi_boot(task) or boot_option == "ramdisk"
or boot_option == "kickstart"):
pxe_utils.prepare_instance_pxe_config(
task, instance_image_info,
iscsi_boot=deploy_utils.is_iscsi_boot(task),
ramdisk_boot=(boot_option == "ramdisk"),
anaconda_boot=(boot_option == "kickstart"),
ipxe_enabled=self.ipxe_enabled)
pxe_utils.prepare_instance_kickstart_config(
task, instance_image_info,
anaconda_boot=(boot_option == "kickstart"))
boot_device = boot_devices.PXE
elif boot_option != "local":

View File

@ -22,3 +22,8 @@ append tboot.gz --- {{pxe_options.aki_path}} root={{ ROOT }} ro text {{ pxe_opti
label boot_ramdisk
kernel {{ pxe_options.aki_path }}
append initrd={{ pxe_options.ari_path }} root=/dev/ram0 text {{ pxe_options.pxe_append_params|default("", true) }} {{ pxe_options.ramdisk_opts|default('', true) }}
label boot_anaconda
kernel {{ pxe_options.aki_path }}
append initrd={{ pxe_options.ari_path }} text {{ pxe_options.pxe_append_params|default("", true) }} inst.ks={{ pxe_options.ks_cfg_url }} inst.stage2={{ pxe_options.stage2_url }}
ipappend 2

View File

@ -62,6 +62,8 @@ class TestPXEUtils(db_base.DbTestCase):
'ipa-api-url': 'http://192.168.122.184:6385',
'ipxe_timeout': 0,
'ramdisk_opts': 'ramdisk_param',
'ks_cfg_url': 'http://fake/ks.cfg',
'stage2_url': 'http://fake/stage2'
}
self.ipxe_options = self.pxe_options.copy()
@ -1144,6 +1146,81 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
boot_opt_mock.assert_called_once_with(task.node)
@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_boot_option(
self, image_show_mock, boot_opt_mock):
properties = {'properties': {u'kernel_id': u'instance_kernel_uuid',
u'ramdisk_id': u'instance_ramdisk_uuid',
u'stage2_id': u'instance_stage2_id'}}
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')),
'stage2':
('instance_stage2_id',
os.path.join(CONF.deploy.http_root,
self.node.uuid,
'LiveOS',
'squashfs.img')),
'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:
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',
u'ramdisk_id': u'instance_ramdisk_uuid'}}
image_show_mock.return_value = properties
self.context.auth_token = 'fake'
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(
exception.ImageUnacceptable, pxe_utils.get_instance_image_info,
task, ipxe_enabled=False
)
@mock.patch.object(deploy_utils, 'fetch_images', autospec=True)
def test__cache_tftp_images_master_path(self, mock_fetch_image):
temp_dir = tempfile.mkdtemp()
@ -1242,6 +1319,68 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
self.assertFalse(mock_log.called)
@mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None)
class PXEBuildKickstartConfigOptionsTestCase(db_base.DbTestCase):
def setUp(self):
super(PXEBuildKickstartConfigOptionsTestCase, self).setUp()
n = {
'driver': 'fake-hardware',
'boot_interface': 'pxe',
'instance_info': INST_INFO_DICT,
'driver_info': DRV_INFO_DICT,
'driver_internal_info': DRV_INTERNAL_INFO_DICT,
}
n['instance_info']['image_url'] = 'http://ironic/node/os_image.tar'
self.config_temp_dir('http_root', group='deploy')
self.node = object_utils.create_test_node(self.context, **n)
@mock.patch.object(deploy_utils, 'get_ironic_api_url', autospec=True)
def test_build_kickstart_config_options_pxe(self, api_url_mock):
api_url_mock.return_value = 'http://ironic-api'
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
expected = {}
expected['liveimg_url'] = task.node.instance_info['image_url']
expected['heartbeat_url'] = (
'http://ironic-api/v1/heartbeat/%s' % task.node.uuid
)
ks_options = pxe_utils.build_kickstart_config_options(task)
self.assertTrue(ks_options.pop('agent_token'))
self.assertEqual(expected, ks_options)
@mock.patch('ironic.common.utils.render_template', autospec=True)
def test_prepare_instance_kickstart_config_not_anaconda_boot(self,
render_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertFalse(
pxe_utils.prepare_instance_kickstart_config(task, {})
)
render_mock.assert_not_called()
@mock.patch('ironic.common.utils.render_template', autospec=True)
@mock.patch('ironic.common.pxe_utils.build_kickstart_config_options',
autospec=True)
@mock.patch('ironic.common.utils.write_to_file', autospec=True)
def test_prepare_instance_kickstart_config(self, write_mock,
ks_options_mock, render_mock):
image_info = {
'ks_cfg': ['', '/http_root/node_uuid/ks.cfg'],
'ks_template': ['tmpl_id', '/http_root/node_uuid/ks.cfg.template']
}
ks_options = {'liveimg_url': 'http://fake', 'agent_token': 'faketoken',
'heartbeat_url': 'http://fake_hb'}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
ks_options_mock.return_value = ks_options
pxe_utils.prepare_instance_kickstart_config(task, image_info,
anaconda_boot=True)
render_mock.assert_called_with(image_info['ks_template'][1],
ks_options)
write_mock.assert_called_with(image_info['ks_cfg'][1],
render_mock.return_value)
@mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None)
class PXEBuildConfigOptionsTestCase(db_base.DbTestCase):
def setUp(self):

View File

@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd
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.stage2=http://fake/stage2 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

View File

@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd
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.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda
initrd http://1.2.3.4:1234/ramdisk || goto boot_anaconda
boot
:boot_ramdisk
imgfree
sanboot http://1.2.3.4:1234/uuid/iso

View File

@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd
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.stage2=http://fake/stage2 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

View File

@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd
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.stage2=http://fake/stage2 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

View File

@ -31,6 +31,12 @@ kernel http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_param initrd=ramd
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.stage2=http://fake/stage2 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

View File

@ -31,6 +31,12 @@ kernel --timeout 120 http://1.2.3.4:1234/kernel root={{ ROOT }} ro text test_par
initrd --timeout 120 http://1.2.3.4:1234/ramdisk || goto boot_partition
boot
:boot_anaconda
imgfree
kernel --timeout 120 http://1.2.3.4:1234/kernel text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2 initrd=ramdisk || goto boot_anaconda
initrd --timeout 120 http://1.2.3.4:1234/ramdisk || goto boot_anaconda
boot
:boot_ramdisk
imgfree
kernel --timeout 120 http://1.2.3.4:1234/kernel root=/dev/ram0 text test_param ramdisk_param initrd=ramdisk || goto boot_ramdisk

View File

@ -600,7 +600,8 @@ class iPXEBootTestCase(db_base.DbTestCase):
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50",
'bios', False, False, False, False, ipxe_enabled=True)
'bios', False, False, False, False, ipxe_enabled=True,
anaconda_boot=False)
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)
@ -649,7 +650,8 @@ class iPXEBootTestCase(db_base.DbTestCase):
ipxe_enabled=True)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50",
'bios', False, False, False, False, ipxe_enabled=True)
'bios', False, False, False, False, ipxe_enabled=True,
anaconda_boot=False)
self.assertFalse(set_boot_device_mock.called)
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@ -766,7 +768,8 @@ class iPXEBootTestCase(db_base.DbTestCase):
ipxe_enabled=True)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, None, boot_modes.LEGACY_BIOS, False,
ipxe_enabled=True, iscsi_boot=True, ramdisk_boot=False)
ipxe_enabled=True, iscsi_boot=True, ramdisk_boot=False,
anaconda_boot=False)
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)
@ -812,7 +815,8 @@ class iPXEBootTestCase(db_base.DbTestCase):
ipxe_enabled=True)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, None, boot_modes.LEGACY_BIOS, False,
ipxe_enabled=True, iscsi_boot=False, ramdisk_boot=True)
ipxe_enabled=True, iscsi_boot=False, ramdisk_boot=True,
anaconda_boot=False)
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)
@ -880,7 +884,8 @@ class iPXEBootTestCase(db_base.DbTestCase):
persistent=True)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50",
'bios', True, False, False, False, ipxe_enabled=True)
'bios', True, False, False, False, ipxe_enabled=True,
anaconda_boot=False)
# No clean up
self.assertFalse(clean_up_pxe_config_mock.called)
# No netboot configuration beyond the PXE files

View File

@ -74,6 +74,7 @@ class PXEBootTestCase(db_base.DbTestCase):
group='anaconda')
instance_info = INST_INFO_DICT
instance_info['deploy_key'] = 'fake-56789'
instance_info['image_url'] = 'http://fakeserver/os.tar.gz'
self.config(enabled_boot_interfaces=[self.boot_interface,
'ipxe', 'fake'])
@ -527,7 +528,8 @@ class PXEBootTestCase(db_base.DbTestCase):
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50",
'bios', False, False, False, False, ipxe_enabled=False)
'bios', False, False, False, False, ipxe_enabled=False,
anaconda_boot=False)
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)
@ -575,7 +577,8 @@ class PXEBootTestCase(db_base.DbTestCase):
ipxe_enabled=False)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50",
'bios', False, False, False, False, ipxe_enabled=False)
'bios', False, False, False, False, ipxe_enabled=False,
anaconda_boot=False)
self.assertFalse(set_boot_device_mock.called)
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@ -727,7 +730,7 @@ class PXEBootTestCase(db_base.DbTestCase):
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, None,
'bios', False, ipxe_enabled=False, iscsi_boot=False,
ramdisk_boot=True)
ramdisk_boot=True, anaconda_boot=False)
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)
@ -740,6 +743,63 @@ class PXEBootTestCase(db_base.DbTestCase):
def test_prepare_instance_ramdisk_pxe_conf_exists(self):
self._test_prepare_instance_ramdisk(config_file_exits=False)
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True)
@mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True)
@mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True)
@mock.patch.object(pxe_utils, 'cache_ramdisk_kernel', autospec=True)
@mock.patch.object(pxe_utils, 'get_instance_image_info', autospec=True)
@mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option',
return_value='kickstart', autospec=True)
@mock.patch('ironic.drivers.modules.deploy_utils.get_ironic_api_url',
return_value='http://fakeserver/api', autospec=True)
@mock.patch('ironic.common.utils.render_template', autospec=True)
@mock.patch('ironic.common.utils.write_to_file', autospec=True)
def test_prepare_instance_kickstart(
self, write_file_mock, render_mock, api_url_mock, boot_opt_mock,
get_image_info_mock, cache_mock, dhcp_factory_mock,
create_pxe_config_mock, switch_pxe_config_mock,
set_boot_device_mock):
image_info = {'kernel': ['ins_kernel_id', '/path/to/kernel'],
'ramdisk': ['ins_ramdisk_id', '/path/to/ramdisk'],
'stage2': ['ins_stage2_id', '/path/to/stage2'],
'ks_cfg': ['', '/path/to/ks.cfg'],
'ks_template': ['template_id', '/path/to/ks_template']}
get_image_info_mock.return_value = image_info
provider_mock = mock.MagicMock()
dhcp_factory_mock.return_value = provider_mock
self.node.provision_state = states.DEPLOYING
self.config(http_url='http://fake_url', group='deploy')
with task_manager.acquire(self.context, self.node.uuid) as task:
dhcp_opts = pxe_utils.dhcp_options_for_instance(
task, ipxe_enabled=False)
dhcp_opts += pxe_utils.dhcp_options_for_instance(
task, ipxe_enabled=False, ip_version=6)
pxe_config_path = pxe_utils.get_pxe_config_file_path(
task.node.uuid)
task.driver.boot.prepare_instance(task)
get_image_info_mock.assert_called_once_with(task,
ipxe_enabled=False)
cache_mock.assert_called_once_with(
task, image_info, False)
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
render_mock.assert_called()
write_file_mock.assert_called_with(
'/path/to/ks.cfg', render_mock.return_value
)
create_pxe_config_mock.assert_called_once_with(
task, mock.ANY, CONF.pxe.pxe_config_template,
ipxe_enabled=False)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, None,
'bios', False, ipxe_enabled=False, iscsi_boot=False,
ramdisk_boot=False, anaconda_boot=True)
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)
@mock.patch.object(boot_mode_utils, 'deconfigure_secure_boot_if_needed',
autospec=True)
@mock.patch.object(pxe_utils, 'clean_up_pxe_env', autospec=True)
@ -826,7 +886,7 @@ class PXERamdiskDeployTestCase(db_base.DbTestCase):
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, None,
'bios', False, ipxe_enabled=False, iscsi_boot=False,
ramdisk_boot=True)
ramdisk_boot=True, anaconda_boot=False)
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)

View File

@ -22,3 +22,8 @@ append tboot.gz --- /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel root={
label boot_ramdisk
kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel
append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk root=/dev/ram0 text test_param ramdisk_param
label boot_anaconda
kernel /tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/kernel
append initrd=/tftpboot/1be26c0b-03f2-4d2e-ae87-c02d7f33c123/ramdisk text test_param inst.ks=http://fake/ks.cfg inst.stage2=http://fake/stage2
ipappend 2