summaryrefslogtreecommitdiff
path: root/ironic
diff options
context:
space:
mode:
authorArun S A G <sagarun@gmail.com>2021-02-16 02:16:32 -0800
committerArun S A G <sagarun@gmail.com>2021-03-23 21:53:34 -0700
commit880bd639f38ba8128e4fb703be0b4a773af83607 (patch)
tree18408a7780e8423aa9b850e10abef1be4c7917df /ironic
parent709562731c3f70e04f96be467c3212b141014fc4 (diff)
downloadironic-880bd639f38ba8128e4fb703be0b4a773af83607.tar.gz
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
Diffstat (limited to 'ironic')
-rw-r--r--ironic/common/pxe_utils.py172
-rw-r--r--ironic/drivers/modules/deploy_utils.py13
-rw-r--r--ironic/drivers/modules/ipxe_config.template6
-rw-r--r--ironic/drivers/modules/pxe_base.py9
-rw-r--r--ironic/drivers/modules/pxe_config.template5
-rw-r--r--ironic/tests/unit/common/test_pxe_utils.py139
-rw-r--r--ironic/tests/unit/drivers/ipxe_config.template6
-rw-r--r--ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template6
-rw-r--r--ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template6
-rw-r--r--ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template6
-rw-r--r--ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template6
-rw-r--r--ironic/tests/unit/drivers/ipxe_config_timeout.template6
-rw-r--r--ironic/tests/unit/drivers/modules/test_ipxe.py15
-rw-r--r--ironic/tests/unit/drivers/modules/test_pxe.py68
-rw-r--r--ironic/tests/unit/drivers/pxe_config.template5
15 files changed, 441 insertions, 27 deletions
diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py
index b05306a16..b670d983d 100644
--- a/ironic/common/pxe_utils.py
+++ b/ironic/common/pxe_utils.py
@@ -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)
diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py
index 432bddbab..ba8ebbebd 100644
--- a/ironic/drivers/modules/deploy_utils.py
+++ b/ironic/drivers/modules/deploy_utils.py
@@ -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=''):
diff --git a/ironic/drivers/modules/ipxe_config.template b/ironic/drivers/modules/ipxe_config.template
index 1321a2a0d..32afea5ec 100644
--- a/ironic/drivers/modules/ipxe_config.template
+++ b/ironic/drivers/modules/ipxe_config.template
@@ -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 %}
diff --git a/ironic/drivers/modules/pxe_base.py b/ironic/drivers/modules/pxe_base.py
index c0b04309b..d0c3a5e4a 100644
--- a/ironic/drivers/modules/pxe_base.py
+++ b/ironic/drivers/modules/pxe_base.py
@@ -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":
diff --git a/ironic/drivers/modules/pxe_config.template b/ironic/drivers/modules/pxe_config.template
index e60be1392..46597403b 100644
--- a/ironic/drivers/modules/pxe_config.template
+++ b/ironic/drivers/modules/pxe_config.template
@@ -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
diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py
index ce56eb276..c7e5d7630 100644
--- a/ironic/tests/unit/common/test_pxe_utils.py
+++ b/ironic/tests/unit/common/test_pxe_utils.py
@@ -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()
@@ -1243,6 +1320,68 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
@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):
super(PXEBuildConfigOptionsTestCase, self).setUp()
diff --git a/ironic/tests/unit/drivers/ipxe_config.template b/ironic/tests/unit/drivers/ipxe_config.template
index 2f1eb098a..70f8a03f1 100644
--- a/ironic/tests/unit/drivers/ipxe_config.template
+++ b/ironic/tests/unit/drivers/ipxe_config.template
@@ -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
diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template
index 2a8e79d17..c7133c7b6 100644
--- a/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template
+++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_iso.template
@@ -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
diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template
index f2a486747..0a872804a 100644
--- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template
+++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_extra_volume.template
@@ -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
diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template
index f5027b3af..571216e39 100644
--- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template
+++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_multipath.template
@@ -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
diff --git a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template
index 9308a5736..6b7a4394d 100644
--- a/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template
+++ b/ironic/tests/unit/drivers/ipxe_config_boot_from_volume_no_extra_volumes.template
@@ -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
diff --git a/ironic/tests/unit/drivers/ipxe_config_timeout.template b/ironic/tests/unit/drivers/ipxe_config_timeout.template
index 90b0b4301..2458f010b 100644
--- a/ironic/tests/unit/drivers/ipxe_config_timeout.template
+++ b/ironic/tests/unit/drivers/ipxe_config_timeout.template
@@ -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
diff --git a/ironic/tests/unit/drivers/modules/test_ipxe.py b/ironic/tests/unit/drivers/modules/test_ipxe.py
index 5229cc250..e57692268 100644
--- a/ironic/tests/unit/drivers/modules/test_ipxe.py
+++ b/ironic/tests/unit/drivers/modules/test_ipxe.py
@@ -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
diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py
index d9fdb63ad..a204f6954 100644
--- a/ironic/tests/unit/drivers/modules/test_pxe.py
+++ b/ironic/tests/unit/drivers/modules/test_pxe.py
@@ -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)
diff --git a/ironic/tests/unit/drivers/pxe_config.template b/ironic/tests/unit/drivers/pxe_config.template
index a94a816ae..b3cfa7ea0 100644
--- a/ironic/tests/unit/drivers/pxe_config.template
+++ b/ironic/tests/unit/drivers/pxe_config.template
@@ -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