diff --git a/ironic/drivers/ilo.py b/ironic/drivers/ilo.py index ff24c634fa..3889bb1479 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 08cfdfd80c..2ba5740b1f 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 3f36d4e695..4f71f0dc73 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 67a92d2db9..d77a6594be 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 1825e1c80b..06025e53a4 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 ec68da5e0d..25a2321ea5 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 d76697c406..5eb0964717 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 eb48eff0ce..685d7ba18d 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 8a1b8e77c0..dcb3a33c92 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