Merge "Add anaconda configuration and template"
This commit is contained in:
commit
8251885db5
@ -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
36
ironic/conf/anaconda.py
Normal 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')
|
@ -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()),
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
||||
|
37
ironic/drivers/modules/ks.cfg.template
Normal file
37
ironic/drivers/modules/ks.cfg.template
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'])
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user