From 5894ff356ec788c8ed93e53f656dc7353148c444 Mon Sep 17 00:00:00 2001 From: Ramakrishnan G Date: Fri, 8 Aug 2014 23:18:59 +0530 Subject: IloVirtualMediaAgent deploy driver This commit introduces a new deploy driver which uses iLO virtual media to boot up proliant baremetal nodes, and uses agent to deploy the baremetal nodes. This patch also changes agent pxe config template slightly, so that the names of agent ramdisk parameters generated in code and those expected by agent ramdisk are same. Change-Id: Ia5677dff294bc146b864bed180fbda939cf9bb38 Implements: blueprint ilo-virtualmedia-ipa --- ironic/drivers/ilo.py | 24 ++++++++ ironic/drivers/modules/agent.py | 38 +++++++++++-- ironic/drivers/modules/agent_config.template | 2 +- ironic/drivers/modules/ilo/deploy.py | 76 ++++++++++++++++++++++++++ ironic/tests/conductor/test_manager.py | 10 ++++ ironic/tests/drivers/agent_pxe_config.template | 2 +- ironic/tests/drivers/ilo/test_deploy.py | 54 ++++++++++++++++++ ironic/tests/drivers/test_agent.py | 19 +++++++ setup.cfg | 1 + 9 files changed, 219 insertions(+), 7 deletions(-) diff --git a/ironic/drivers/ilo.py b/ironic/drivers/ilo.py index ff24c634f..3889bb147 100644 --- a/ironic/drivers/ilo.py +++ b/ironic/drivers/ilo.py @@ -20,6 +20,7 @@ from oslo.utils import importutils from ironic.common import exception from ironic.common.i18n import _ from ironic.drivers import base +from ironic.drivers.modules import agent from ironic.drivers.modules.ilo import deploy from ironic.drivers.modules.ilo import power from ironic.drivers.modules import ipmitool @@ -46,3 +47,26 @@ class IloVirtualMediaIscsiDriver(base.BaseDriver): self.console = ipmitool.IPMIShellinaboxConsole() self.management = ipmitool.IPMIManagement() self.vendor = deploy.VendorPassthru() + + +class IloVirtualMediaAgentDriver(base.BaseDriver): + """IloDriver using IloClient interface. + + This driver implements the `core` functionality using + :class:ironic.drivers.modules.ilo.power.IloPower for power management + and + :class:ironic.drivers.modules.ilo.deploy.IloVirtualMediaAgentDriver for + deploy. + """ + + def __init__(self): + if not importutils.try_import('proliantutils'): + raise exception.DriverLoadError( + driver=self.__class__.__name__, + reason=_("Unable to import proliantutils library")) + + self.power = power.IloPower() + self.deploy = deploy.IloVirtualMediaAgentDeploy() + self.console = ipmitool.IPMIShellinaboxConsole() + self.management = ipmitool.IPMIManagement() + self.vendor = agent.AgentVendorInterface() diff --git a/ironic/drivers/modules/agent.py b/ironic/drivers/modules/agent.py index 08cfdfd80..2ba5740b1 100644 --- a/ironic/drivers/modules/agent.py +++ b/ironic/drivers/modules/agent.py @@ -74,15 +74,38 @@ def _get_client(): return client -def _build_pxe_config_options(pxe_info): +def build_agent_options(): + """Build the options to be passed to the agent ramdisk. + + :returns: a dictionary containing the parameters to be passed to + agent ramdisk. + """ ironic_api = (CONF.conductor.api_url or keystone.get_service_url()).rstrip('/') return { + 'ipa-api-url': ironic_api, + } + + +def _build_pxe_config_options(pxe_info): + """Builds the pxe config options for booting agent. + + This method builds the config options to be replaced on + the agent pxe config template. + + :param pxe_info: A dict containing the 'deploy_kernel' and + 'deploy_ramdisk' for the agent pxe config template. + :returns: a dict containing the options to be applied on + the agent pxe config template. + """ + agent_config_opts = { 'deployment_aki_path': pxe_info['deploy_kernel'][1], 'deployment_ari_path': pxe_info['deploy_ramdisk'][1], 'pxe_append_params': CONF.agent.agent_pxe_append_params, - 'ipa_api_url': ironic_api, } + agent_opts = build_agent_options() + agent_config_opts.update(agent_opts) + return agent_config_opts def _get_tftp_image_info(node): @@ -162,8 +185,13 @@ def _cache_tftp_images(ctx, node, pxe_info): _fetch_images(ctx, AgentTFTPImageCache(), pxe_info.values()) -def _build_instance_info_for_deploy(task): - """Build instance_info necessary for deploying to a node.""" +def build_instance_info_for_deploy(task): + """Build instance_info necessary for deploying to a node. + + :param task: a TaskManager object containing the node + :returns: a dictionary containing the properties to be updated + in instance_info + """ node = task.node instance_info = node.instance_info @@ -248,7 +276,7 @@ class AgentDeploy(base.DeployInterface): CONF.agent.agent_pxe_config_template) _cache_tftp_images(task.context, node, pxe_info) - node.instance_info = _build_instance_info_for_deploy(task) + node.instance_info = build_instance_info_for_deploy(task) node.save(task.context) def clean_up(self, task): diff --git a/ironic/drivers/modules/agent_config.template b/ironic/drivers/modules/agent_config.template index 3f36d4e69..4f71f0dc7 100644 --- a/ironic/drivers/modules/agent_config.template +++ b/ironic/drivers/modules/agent_config.template @@ -2,4 +2,4 @@ default deploy label deploy kernel {{ pxe_options.deployment_aki_path }} -append initrd={{ pxe_options.deployment_ari_path }} text {{ pxe_options.pxe_append_params }} {% if pxe_options.ipa_api_url %}ipa-api-url={{ pxe_options.ipa_api_url }}{% endif %} {% if pxe_options.ipa_advertise_host %}ipa-advertise-host={{ pxe_options.ipa_advertise_host }}{% endif %} +append initrd={{ pxe_options.deployment_ari_path }} text {{ pxe_options.pxe_append_params }} {% if pxe_options.ipa-api-url %}ipa-api-url={{ pxe_options.ipa-api-url }}{% endif %} {% if pxe_options.ipa-advertise-host %}ipa-advertise-host={{ pxe_options.ipa-advertise-host }}{% endif %} diff --git a/ironic/drivers/modules/ilo/deploy.py b/ironic/drivers/modules/ilo/deploy.py index 67a92d2db..d77a6594b 100644 --- a/ironic/drivers/modules/ilo/deploy.py +++ b/ironic/drivers/modules/ilo/deploy.py @@ -28,6 +28,7 @@ from ironic.common import swift from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils from ironic.drivers import base +from ironic.drivers.modules import agent from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import common as ilo_common from ironic.drivers.modules import iscsi_deploy @@ -306,6 +307,81 @@ class IloVirtualMediaIscsiDeploy(base.DeployInterface): pass +class IloVirtualMediaAgentDeploy(base.DeployInterface): + """Interface for deploy-related actions.""" + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return COMMON_PROPERTIES + + def validate(self, task): + """Validate the driver-specific Node deployment info. + + :param task: a TaskManager instance + :raises: MissingParameterValue if some parameters are missing. + """ + _parse_driver_info(task.node) + + @task_manager.require_exclusive_lock + def deploy(self, task): + """Perform a deployment to a node. + + Prepares the options for the agent ramdisk and sets the node to boot + from virtual media cdrom. + + :param task: a TaskManager instance. + :returns: states.DEPLOYWAIT + :raises: ImageCreationFailed, if it failed while creating the floppy + image. + :raises: IloOperationError, if some operation on iLO fails. + """ + deploy_ramdisk_opts = agent.build_agent_options() + deploy_iso_uuid = task.node.driver_info['ilo_deploy_iso'] + deploy_iso = 'glance:' + deploy_iso_uuid + _reboot_into(task, deploy_iso, deploy_ramdisk_opts) + + return states.DEPLOYWAIT + + @task_manager.require_exclusive_lock + def tear_down(self, task): + """Tear down a previous deployment on the task's node. + + :param task: a TaskManager instance. + :returns: states.DELETED + """ + manager_utils.node_power_action(task, states.POWER_OFF) + return states.DELETED + + def prepare(self, task): + """Prepare the deployment environment for this node. + + :param task: a TaskManager instance. + """ + node = task.node + node.instance_info = agent.build_instance_info_for_deploy(task) + node.save(task.context) + + def clean_up(self, task): + """Clean up the deployment environment for this node. + + Ejects the attached virtual media from the iLO and also removes + the floppy image from Swift, if it exists. + + :param task: a TaskManager instance. + """ + ilo_common.cleanup_vmedia_boot(task) + + def take_over(self, task): + """Take over management of this node from a dead conductor. + + :param task: a TaskManager instance. + """ + pass + + class VendorPassthru(base.VendorInterface): """Vendor-specific interfaces for iLO deploy drivers.""" diff --git a/ironic/tests/conductor/test_manager.py b/ironic/tests/conductor/test_manager.py index 1825e1c80..06025e53a 100644 --- a/ironic/tests/conductor/test_manager.py +++ b/ironic/tests/conductor/test_manager.py @@ -2269,6 +2269,16 @@ class ManagerTestProperties(tests_db_base.DbTestCase): 'ipmi_target_address', 'ipmi_local_address'] self._check_driver_properties("iscsi_ilo", expected) + def test_driver_properties_agent_ilo(self): + expected = ['ilo_address', 'ilo_username', 'ilo_password', + 'client_port', 'client_timeout', 'ilo_deploy_iso', + 'ipmi_address', 'ipmi_terminal_port', + 'ipmi_password', 'ipmi_priv_level', + 'ipmi_username', 'ipmi_bridging', 'ipmi_transit_channel', + 'ipmi_transit_address', 'ipmi_target_channel', + 'ipmi_target_address', 'ipmi_local_address'] + self._check_driver_properties("agent_ilo", expected) + def test_driver_properties_fail(self): mgr_utils.mock_the_extension_manager() self.driver = driver_factory.get_driver("fake") diff --git a/ironic/tests/drivers/agent_pxe_config.template b/ironic/tests/drivers/agent_pxe_config.template index ec68da5e0..25a2321ea 100644 --- a/ironic/tests/drivers/agent_pxe_config.template +++ b/ironic/tests/drivers/agent_pxe_config.template @@ -2,4 +2,4 @@ default deploy label deploy kernel {{ pxe_options.deployment_aki_path }} -append initrd={{ pxe_options.deployment_ari_path }} root=squashfs: {% if pxe_options.pxe_append_params %}{{ pxe_options.pxe_append_params }}{% endif %} state=tmpfs: ipa-api-url={{ pxe_options.ipa_api_url }} {% if pxe_options.ipa_advertise_host %}ipa-advertise-host={{ pxe_options.ipa_advertise_host }}{% endif %} +append initrd={{ pxe_options.deployment_ari_path }} text root=squashfs: {% if pxe_options.pxe_append_params %}{{ pxe_options.pxe_append_params }}{% endif %} state=tmpfs: ipa-api-url={{ pxe_options.ipa-api-url }} {% if pxe_options.ipa-advertise-host %}ipa-advertise-host={{ pxe_options.ipa-advertise-host }}{% endif %} diff --git a/ironic/tests/drivers/ilo/test_deploy.py b/ironic/tests/drivers/ilo/test_deploy.py index d76697c40..5eb096471 100644 --- a/ironic/tests/drivers/ilo/test_deploy.py +++ b/ironic/tests/drivers/ilo/test_deploy.py @@ -27,6 +27,7 @@ from ironic.common import utils from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils from ironic.db import api as dbapi +from ironic.drivers.modules import agent from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import common as ilo_common from ironic.drivers.modules.ilo import deploy as ilo_deploy @@ -249,6 +250,59 @@ class IloVirtualMediaIscsiDeployTestCase(base.TestCase): clean_up_boot_mock.assert_called_once_with(task.node) +class IloVirtualMediaAgentDeployTestCase(base.TestCase): + + def setUp(self): + super(IloVirtualMediaAgentDeployTestCase, self).setUp() + self.dbapi = dbapi.get_instance() + self.context = context.get_admin_context() + mgr_utils.mock_the_extension_manager(driver="agent_ilo") + self.node = obj_utils.create_test_node(self.context, + driver='agent_ilo', driver_info=INFO_DICT) + + @mock.patch.object(ilo_deploy, '_parse_driver_info') + def test_validate(self, parse_driver_info_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.validate(task) + parse_driver_info_mock.assert_called_once_with(task.node) + + @mock.patch.object(ilo_deploy, '_reboot_into') + @mock.patch.object(agent, 'build_agent_options') + def test_deploy(self, build_options_mock, reboot_into_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + deploy_opts = {'a': 'b'} + build_options_mock.return_value = deploy_opts + task.node.driver_info['ilo_deploy_iso'] = 'deploy-iso-uuid' + + returned_state = task.driver.deploy.deploy(task) + + build_options_mock.assert_called_once_with() + reboot_into_mock.assert_called_once_with(task, + 'glance:deploy-iso-uuid', + deploy_opts) + self.assertEqual(states.DEPLOYWAIT, returned_state) + + @mock.patch.object(manager_utils, 'node_power_action') + def test_tear_down(self, node_power_action_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + returned_state = task.driver.deploy.tear_down(task) + node_power_action_mock.assert_called_once_with(task, + states.POWER_OFF) + self.assertEqual(states.DELETED, returned_state) + + @mock.patch.object(agent, 'build_instance_info_for_deploy') + def test_prepare(self, build_instance_info_mock): + deploy_opts = {'a': 'b'} + build_instance_info_mock.return_value = deploy_opts + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy.prepare(task) + self.assertEqual(deploy_opts, task.node.instance_info) + + class VendorPassthruTestCase(base.TestCase): def setUp(self): diff --git a/ironic/tests/drivers/test_agent.py b/ironic/tests/drivers/test_agent.py index eb48eff0c..685d7ba18 100644 --- a/ironic/tests/drivers/test_agent.py +++ b/ironic/tests/drivers/test_agent.py @@ -17,6 +17,7 @@ from oslo.config import cfg from ironic.common import dhcp_factory from ironic.common import exception +from ironic.common import keystone from ironic.common import pxe_utils from ironic.common import states from ironic.conductor import task_manager @@ -35,6 +36,24 @@ DRIVER_INFO = db_utils.get_test_agent_driver_info() CONF = cfg.CONF +class TestAgentMethods(db_base.DbTestCase): + def setUp(self): + super(TestAgentMethods, self).setUp() + + def test_build_agent_options_conf(self): + self.config(api_url='api-url', group='conductor') + options = agent.build_agent_options() + self.assertEqual('api-url', options['ipa-api-url']) + + @mock.patch.object(keystone, 'get_service_url') + def test_build_agent_options_keystone(self, get_url_mock): + + self.config(api_url=None, group='conductor') + get_url_mock.return_value = 'api-url' + options = agent.build_agent_options() + self.assertEqual('api-url', options['ipa-api-url']) + + class TestAgentDeploy(db_base.DbTestCase): def setUp(self): super(TestAgentDeploy, self).setUp() diff --git a/setup.cfg b/setup.cfg index 8a1b8e77c..dcb3a33c9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ ironic.dhcp = none = ironic.dhcp.none:NoneDHCPApi ironic.drivers = + agent_ilo = ironic.drivers.ilo:IloVirtualMediaAgentDriver agent_ipmitool = ironic.drivers.agent:AgentAndIPMIToolDriver agent_pyghmi = ironic.drivers.agent:AgentAndIPMINativeDriver agent_ssh = ironic.drivers.agent:AgentAndSSHDriver -- cgit v1.2.1