Add anaconda configuration and template

This change adds 'anaconda' group and 'default_ks_template'
configuration option under that group to ironic configuration file.
Along with this change a new boot_option named 'kickstart' is added
to identify anaconda kickstart deploy in the boot interface.

deploy_utils.get_boot_option method is modified to check if
node.deploy_interface is set to 'anaconda' and return boot_option
'kickstart'.

This change also validates whether required parameters are set when
the boot_option on the node is set to 'kickstart'.

When boot_option is 'kickstart' we also validate if the glance image
source has 'squashfs_id' property associated with it.

Change-Id: I2ef7c33e2e63e6d08c084b4c5dbd77a44ddd2d14
Story: 2007839
Task: 41675
This commit is contained in:
Arun S A G 2021-01-23 11:56:51 -08:00
parent 121bc5a4c2
commit 26040d4563
11 changed files with 173 additions and 3 deletions

View File

@ -16,6 +16,7 @@
from oslo_config import cfg
from ironic.conf import agent
from ironic.conf import anaconda
from ironic.conf import ansible
from ironic.conf import api
from ironic.conf import audit
@ -68,6 +69,7 @@ inspector.register_opts(CONF)
ipmi.register_opts(CONF)
irmc.register_opts(CONF)
iscsi.register_opts(CONF)
anaconda.register_opts(CONF)
metrics.register_opts(CONF)
metrics_statsd.register_opts(CONF)
neutron.register_opts(CONF)

36
ironic/conf/anaconda.py Normal file
View File

@ -0,0 +1,36 @@
# Copyright 2021 Verizon Media
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from oslo_config import cfg
from ironic.common.i18n import _
ks_group = cfg.OptGroup(name='anaconda',
title='Anaconda/kickstart interface options')
opts = [
cfg.StrOpt('default_ks_template',
default=os.path.join(
'$pybasedir', 'drivers/modules/ks.cfg.template'),
mutable=True,
help=_('kickstart template to use when no kickstart template '
'is specified in the instance_info or the glance OS '
'image.')),
]
def register_opts(conf):
conf.register_group(ks_group)
conf.register_opts(opts, group='anaconda')

View File

@ -51,6 +51,7 @@ _opts = [
('ipmi', ironic.conf.ipmi.opts),
('irmc', ironic.conf.irmc.opts),
('iscsi', ironic.conf.iscsi.opts),
('anaconda', ironic.conf.anaconda.opts),
('metrics', ironic.conf.metrics.opts),
('metrics_statsd', ironic.conf.metrics_statsd.opts),
('neutron', ironic.conf.neutron.list_opts()),

View File

@ -50,7 +50,8 @@ class GenericHardware(hardware_type.AbstractHardwareType):
def supported_deploy_interfaces(self):
"""List of supported deploy interfaces."""
return [agent.AgentDeploy, iscsi_deploy.ISCSIDeploy,
ansible_deploy.AnsibleDeploy, pxe.PXERamdiskDeploy]
ansible_deploy.AnsibleDeploy, pxe.PXERamdiskDeploy,
pxe.PXEAnacondaDeploy]
@property
def supported_inspect_interfaces(self):

View File

@ -55,7 +55,7 @@ LOG = logging.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__name__)
SUPPORTED_CAPABILITIES = {
'boot_option': ('local', 'netboot', 'ramdisk'),
'boot_option': ('local', 'netboot', 'ramdisk', 'kickstart'),
'boot_mode': ('bios', 'uefi'),
'secure_boot': ('true', 'false'),
'trusted_boot': ('true', 'false'),
@ -581,11 +581,25 @@ def get_boot_option(node):
# NOTE(TheJulia): Software raid always implies local deployment
if is_software_raid(node):
return 'local'
if is_anaconda_deploy(node):
return 'kickstart'
capabilities = utils.parse_instance_info_capabilities(node)
return capabilities.get('boot_option',
CONF.deploy.default_boot_option).lower()
def is_anaconda_deploy(node):
"""Determine if Anaconda deploy interface is in use for the deployment.
:param node: A single Node.
:returns: A boolean value of True when Anaconda deploy interface is in use
otherwise False
"""
if node.deploy_interface == 'anaconda':
return True
return False
def is_software_raid(node):
"""Determine if software raid is in use for the deployment.

View File

@ -0,0 +1,37 @@
lang en_US
keyboard us
timezone UTC --utc
#platform x86, AMD64, or Intel EM64T
text
cmdline
reboot
selinux --enforcing
firewall --enabled
firstboot --disabled
bootloader --location=mbr --append="rhgb quiet crashkernel=auto"
zerombr
clearpart --all --initlabel
autopart
# Downloading and installing OS image using liveimg section is mandatory
liveimg --url {{ ks_options.liveimg_url }}
# Following %pre, %onerror and %trackback sections are mandatory
%pre
/usr/bin/curl -X PUT -H 'Content-Type: application/json' -H 'Accept:application/json' -d '{"agent_token": "{{ ks_options.agent_token }}", "agent_state": "start", "agent_status": "Deployment starting. Running pre-installation scripts."}' {{ ks_options.heartbeat_url }}
%end
%onerror
/usr/bin/curl -X PUT -H 'Content-Type: application/json' -H 'Accept:application/json' -d '{"agent_token": "{{ ks_options.agent_token }}", "agent_state": "error", "agent_status": "Error: Deploying using anaconda. Check console for more information."}' {{ ks_options.heartbeat_url }}
%end
%traceback
/usr/bin/curl -X PUT -H 'Content-Type: application/json' -H 'Accept:application/json' -d '{"agent_token": "{{ ks_options.agent_token }}", "agent_state": "error", "agent_status": "Error: Installer crashed unexpectedly."}' {{ ks_options.heartbeat_url }}
%end
# Sending callback after the installation is mandatory
%post
/usr/bin/curl -X PUT -H 'Content-Type: application/json' -H 'Accept:application/json' -d '{"agent_token": "{{ ks_options.agent_token }}", "agent_state": "end", "agent_status": "Deployment completed successfully."}' {{ ks_options.heartbeat_url }}
%end

View File

@ -116,3 +116,24 @@ class PXERamdiskDeploy(agent_base.AgentBaseMixin, agent_base.HeartbeatMixin,
if node.provision_state in (states.ACTIVE, states.UNRESCUING):
# In the event of takeover or unrescue.
task.driver.boot.prepare_instance(task)
class PXEAnacondaDeploy(agent_base.AgentBaseMixin, agent_base.HeartbeatMixin,
base.DeployInterface):
def get_properties(self, task):
return {}
def validate(self, task):
pass
@METRICS.timer('AnacondaDeploy.deploy')
@base.deploy_step(priority=100)
@task_manager.require_exclusive_lock
def deploy(self, task):
pass
@METRICS.timer('AnacondaDeploy.prepare')
@task_manager.require_exclusive_lock
def prepare(self, task):
pass

View File

@ -331,6 +331,21 @@ class PXEBaseMixin(object):
"iPXE boot is enabled but no HTTP URL or HTTP "
"root was specified."))
# NOTE(zer0c00l): When 'kickstart' boot option is used we need to store
# kickstart and squashfs files in http_root directory. These files
# will be eventually requested by anaconda installer during deployment
# over http(s).
if deploy_utils.get_boot_option(node) == 'kickstart':
if not CONF.deploy.http_url or not CONF.deploy.http_root:
raise exception.MissingParameterValue(_(
"'kickstart' boot option is set on the node but no HTTP "
"URL or HTTP root was specified."))
if not CONF.anaconda.default_ks_template:
raise exception.MissingParameterValue(_(
"'kickstart' boot option is set on the node but no "
"default kickstart template is specified."))
# Check the trusted_boot capabilities value.
deploy_utils.validate_capabilities(node)
if deploy_utils.is_trusted_boot_requested(node):
@ -390,6 +405,8 @@ class PXEBaseMixin(object):
props = ['boot_iso']
elif service_utils.is_glance_image(d_info['image_source']):
props = ['kernel_id', 'ramdisk_id']
if deploy_utils.get_boot_option(node) == 'kickstart':
props.append('squashfs_id')
else:
props = ['kernel', 'ramdisk']
deploy_utils.validate_image_properties(task.context, d_info, props)

View File

@ -772,6 +772,21 @@ class OtherFunctionTestCase(db_base.DbTestCase):
result = utils.get_boot_option(self.node)
self.assertEqual("local", result)
@mock.patch.object(utils, 'is_anaconda_deploy', autospec=True)
def test_get_boot_option_anaconda_deploy(self, mock_is_anaconda_deploy):
mock_is_anaconda_deploy.return_value = True
result = utils.get_boot_option(self.node)
self.assertEqual("kickstart", result)
def test_is_anaconda_deploy(self):
self.node.deploy_interface = 'anaconda'
result = utils.is_anaconda_deploy(self.node)
self.assertTrue(result)
def test_is_anaconda_deploy_false(self):
result = utils.is_anaconda_deploy(self.node)
self.assertFalse(result)
def test_is_software_raid(self):
self.node.target_raid_config = {
"logical_disks": [
@ -989,7 +1004,7 @@ class ParseInstanceInfoCapabilitiesTestCase(tests_base.TestCase):
utils.validate_capabilities, self.node)
def test_all_supported_capabilities(self):
self.assertEqual(('local', 'netboot', 'ramdisk'),
self.assertEqual(('local', 'netboot', 'ramdisk', 'kickstart'),
utils.SUPPORTED_CAPABILITIES['boot_option'])
self.assertEqual(('bios', 'uefi'),
utils.SUPPORTED_CAPABILITIES['boot_mode'])

View File

@ -70,11 +70,15 @@ class PXEBootTestCase(db_base.DbTestCase):
self.config_temp_dir('tftp_root', group='pxe')
self.config_temp_dir('images_path', group='pxe')
self.config_temp_dir('http_root', group='deploy')
self.config(default_ks_template='/etc/ironic/ks.cfg.template',
group='anaconda')
instance_info = INST_INFO_DICT
instance_info['deploy_key'] = 'fake-56789'
self.config(enabled_boot_interfaces=[self.boot_interface,
'ipxe', 'fake'])
self.config(enabled_deploy_interfaces=['fake', 'direct', 'iscsi',
'anaconda'])
self.node = obj_utils.create_test_node(
self.context,
driver=self.driver,
@ -223,6 +227,27 @@ class PXEBootTestCase(db_base.DbTestCase):
self.assertRaises(exception.UnsupportedDriverExtension,
task.driver.boot.validate_inspection, task)
@mock.patch.object(deploy_utils, 'validate_image_properties',
autospec=True)
def test_validate_kickstart_has_squashfs_id(self, mock_validate_img):
node = self.node
node.deploy_interface = 'anaconda'
node.save()
self.config(http_url='http://fake_url', group='deploy')
with task_manager.acquire(self.context, node.uuid) as task:
task.driver.boot.validate(task)
mock_validate_img.assert_called_once_with(
mock.ANY, mock.ANY, ['kernel_id', 'ramdisk_id', 'squashfs_id']
)
def test_validate_kickstart_fail_http_url_not_set(self):
node = self.node
node.deploy_interface = 'anaconda'
node.save()
with task_manager.acquire(self.context, node.uuid) as task:
self.assertRaises(exception.MissingParameterValue,
task.driver.boot.validate, task)
@mock.patch.object(manager_utils, 'node_get_boot_mode', autospec=True)
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True)

View File

@ -85,6 +85,7 @@ ironic.hardware.interfaces.console =
no-console = ironic.drivers.modules.noop:NoConsole
ironic.hardware.interfaces.deploy =
anaconda = ironic.drivers.modules.pxe:PXEAnacondaDeploy
ansible = ironic.drivers.modules.ansible.deploy:AnsibleDeploy
direct = ironic.drivers.modules.agent:AgentDeploy
fake = ironic.drivers.modules.fake:FakeDeploy