summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArun S A G <sagarun@gmail.com>2021-01-23 11:56:51 -0800
committerArun S A G <sagarun@gmail.com>2021-03-19 09:39:50 -0700
commit26040d4563ca683319d56aaecef478df6ee5390f (patch)
tree08155e27b96d12520a4057c644879a75ffb0d617
parent121bc5a4c2ddb6a542323e55c593817df5805126 (diff)
downloadironic-26040d4563ca683319d56aaecef478df6ee5390f.tar.gz
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
-rw-r--r--ironic/conf/__init__.py2
-rw-r--r--ironic/conf/anaconda.py36
-rw-r--r--ironic/conf/opts.py1
-rw-r--r--ironic/drivers/generic.py3
-rw-r--r--ironic/drivers/modules/deploy_utils.py16
-rw-r--r--ironic/drivers/modules/ks.cfg.template37
-rw-r--r--ironic/drivers/modules/pxe.py21
-rw-r--r--ironic/drivers/modules/pxe_base.py17
-rw-r--r--ironic/tests/unit/drivers/modules/test_deploy_utils.py17
-rw-r--r--ironic/tests/unit/drivers/modules/test_pxe.py25
-rw-r--r--setup.cfg1
11 files changed, 173 insertions, 3 deletions
diff --git a/ironic/conf/__init__.py b/ironic/conf/__init__.py
index 1503fdd21..a1da1fb7e 100644
--- a/ironic/conf/__init__.py
+++ b/ironic/conf/__init__.py
@@ -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)
diff --git a/ironic/conf/anaconda.py b/ironic/conf/anaconda.py
new file mode 100644
index 000000000..8ae3ab533
--- /dev/null
+++ b/ironic/conf/anaconda.py
@@ -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')
diff --git a/ironic/conf/opts.py b/ironic/conf/opts.py
index 1cde057cb..976611be6 100644
--- a/ironic/conf/opts.py
+++ b/ironic/conf/opts.py
@@ -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()),
diff --git a/ironic/drivers/generic.py b/ironic/drivers/generic.py
index 956119df8..e8b359f18 100644
--- a/ironic/drivers/generic.py
+++ b/ironic/drivers/generic.py
@@ -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):
diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py
index bc9a7114e..432bddbab 100644
--- a/ironic/drivers/modules/deploy_utils.py
+++ b/ironic/drivers/modules/deploy_utils.py
@@ -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.
diff --git a/ironic/drivers/modules/ks.cfg.template b/ironic/drivers/modules/ks.cfg.template
new file mode 100644
index 000000000..3d74c4f3c
--- /dev/null
+++ b/ironic/drivers/modules/ks.cfg.template
@@ -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
+
diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py
index 81e04eb75..97f8e5961 100644
--- a/ironic/drivers/modules/pxe.py
+++ b/ironic/drivers/modules/pxe.py
@@ -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
diff --git a/ironic/drivers/modules/pxe_base.py b/ironic/drivers/modules/pxe_base.py
index 70162a2ee..c0b04309b 100644
--- a/ironic/drivers/modules/pxe_base.py
+++ b/ironic/drivers/modules/pxe_base.py
@@ -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)
diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py
index 03eba2011..da2c4e9ff 100644
--- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py
+++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py
@@ -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'])
diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py
index da1d9da97..d9fdb63ad 100644
--- a/ironic/tests/unit/drivers/modules/test_pxe.py
+++ b/ironic/tests/unit/drivers/modules/test_pxe.py
@@ -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)
diff --git a/setup.cfg b/setup.cfg
index 827fc565a..83d308e63 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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