diff options
author | Ruby Loo <rloo@yahoo-inc.com> | 2014-07-15 10:52:32 -0400 |
---|---|---|
committer | Ruby Loo <rloo@yahoo-inc.com> | 2014-07-23 03:21:16 +0000 |
commit | a65d3e6b1126b4e148e825290a61b151e1a6559c (patch) | |
tree | 840bfa561a7b363e5a3dab78f2aac63617a93ef5 /ironic | |
parent | c75a070520b02deafe61343157d3a4219374f6d3 (diff) | |
download | ironic-a65d3e6b1126b4e148e825290a61b151e1a6559c.tar.gz |
Implement API to get driver properties
Implement GET /v1/drivers/<driver>/properties that returns a
dictionary of (driver_info) properties of the specified driver.
Each entry in the dictionary is:
key: name of the property
value: description of the property
An invalid driver name results in an HTTP 404.
Eg: 'GET /v1/drivers/pxe_ipminative/properties' returns:
{"pxe_deploy_ramdisk": "UUID ... of the ramdisk... Required.",
"ipmi_username": "IPMI username. Required.",
"ipmi_address": "IP of the node's BMC. Required.",
"ipmi_password": "IPMI password. Required.",
"pxe_deploy_kernel": "UUID ... of the deployment kernel. Required."
}
If the properties for a driver are not cached, the API service makes
an RPC request to a conductor to get the properties for that driver.
It then caches that information for subsequent requests of that driver.
Change-Id: I9c98f4369c54a7cdf9e20ea87348e61f7af10303
Blueprint: get-required-driver-info
Partial-Bug: #1261915
Diffstat (limited to 'ironic')
-rw-r--r-- | ironic/api/controllers/v1/driver.py | 33 | ||||
-rw-r--r-- | ironic/common/hash_ring.py | 3 | ||||
-rw-r--r-- | ironic/conductor/manager.py | 43 | ||||
-rw-r--r-- | ironic/conductor/rpcapi.py | 18 | ||||
-rw-r--r-- | ironic/tests/api/v1/test_drivers.py | 78 | ||||
-rw-r--r-- | ironic/tests/conductor/test_manager.py | 96 | ||||
-rw-r--r-- | ironic/tests/conductor/test_rpcapi.py | 5 |
7 files changed, 261 insertions, 15 deletions
diff --git a/ironic/api/controllers/v1/driver.py b/ironic/api/controllers/v1/driver.py index 8bca7b209..aa06b5bfb 100644 --- a/ironic/api/controllers/v1/driver.py +++ b/ironic/api/controllers/v1/driver.py @@ -25,6 +25,17 @@ from ironic.api.controllers import link from ironic.common import exception +# Property information for drivers: +# key = driver name; +# value = dictionary of properties of that driver: +# key = property name. +# value = description of the property. +# NOTE(rloo). This is cached for the lifetime of the API service. If one or +# more conductor services are restarted with new driver versions, the API +# service should be restarted. +_DRIVER_PROPERTIES = {} + + class Driver(base.APIBase): """API representation of a driver.""" @@ -113,6 +124,10 @@ class DriversController(rest.RestController): vendor_passthru = DriverPassthruController() + _custom_actions = { + 'properties': ['GET'], + } + @wsme_pecan.wsexpose(DriverList) def get_all(self): """Retrieve a list of drivers. @@ -139,3 +154,21 @@ class DriversController(rest.RestController): return Driver.convert_with_links(name, list(hosts)) raise exception.DriverNotFound(driver_name=driver_name) + + @wsme_pecan.wsexpose(wtypes.text, wtypes.text) + def properties(self, driver_name): + """Retrieve property information of the given driver. + + :param driver_name: name of the driver. + :returns: dictionary with <property name>:<property description> + entries. + :raises: DriverNotFound (HTTP 404) if the driver name is invalid or + the driver cannot be loaded. + """ + if driver_name not in _DRIVER_PROPERTIES: + topic = pecan.request.rpcapi.get_topic_for_driver(driver_name) + properties = pecan.request.rpcapi.get_driver_properties( + pecan.request.context, driver_name, topic=topic) + _DRIVER_PROPERTIES[driver_name] = properties + + return _DRIVER_PROPERTIES[driver_name] diff --git a/ironic/common/hash_ring.py b/ironic/common/hash_ring.py index e5c12f9b9..00f272ffd 100644 --- a/ironic/common/hash_ring.py +++ b/ironic/common/hash_ring.py @@ -142,4 +142,5 @@ class HashRingManager(object): try: return self.hash_rings[driver_name] except KeyError: - raise exception.DriverNotFound(driver_name=driver_name) + raise exception.DriverNotFound(_("The driver '%s' is unknown.") % + driver_name) diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 3b0b6619b..434d2ae9c 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -124,7 +124,7 @@ class ConductorManager(periodic_task.PeriodicTasks): """Ironic Conductor manager main class.""" # NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's. - RPC_API_VERSION = '1.15' + RPC_API_VERSION = '1.16' target = messaging.Target(version=RPC_API_VERSION) @@ -135,11 +135,26 @@ class ConductorManager(periodic_task.PeriodicTasks): self.topic = topic self.power_state_sync_count = collections.defaultdict(int) + def _get_driver(self, driver_name): + """Get the driver. + + :param driver_name: name of the driver. + :returns: the driver; an instance of a class which implements + :class:`ironic.drivers.base.BaseDriver`. + :raises: DriverNotFound if the driver is not loaded. + + """ + try: + return self._driver_factory[driver_name].obj + except KeyError: + raise exception.DriverNotFound(driver_name=driver_name) + def init_host(self): self.dbapi = dbapi.get_instance() - self.driver_factory = driver_factory.DriverFactory() - self.drivers = self.driver_factory.names + self._driver_factory = driver_factory.DriverFactory() + + self.drivers = self._driver_factory.names """List of driver names which this conductor supports.""" try: @@ -320,11 +335,7 @@ class ConductorManager(periodic_task.PeriodicTasks): # Any locking in a top-level vendor action will need to be done by the # implementation, as there is little we could reasonably lock on here. LOG.debug("RPC driver_vendor_passthru for driver %s." % driver_name) - try: - driver = self.driver_factory[driver_name].obj - except KeyError: - raise exception.DriverNotFound(driver_name=driver_name) - + driver = self._get_driver(driver_name) if not getattr(driver, 'vendor', None): raise exception.UnsupportedDriverExtension( driver=driver_name, @@ -960,3 +971,19 @@ class ConductorManager(periodic_task.PeriodicTasks): port_obj.save(context) return port_obj + + @messaging.expected_exceptions(exception.DriverNotFound) + def get_driver_properties(self, context, driver_name): + """Get the properties of the driver. + + :param context: request context. + :param driver_name: name of the driver. + :returns: a dictionary with <property name>:<property description> + entries. + :raises: DriverNotFound if the driver is not loaded. + + """ + LOG.debug("RPC get_driver_properties called for driver %s.", + driver_name) + driver = self._get_driver(driver_name) + return driver.get_properties() diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py index d2b0f885e..cbe69cf19 100644 --- a/ironic/conductor/rpcapi.py +++ b/ironic/conductor/rpcapi.py @@ -53,11 +53,12 @@ class ConductorAPI(object): 1.13 - Added update_port. 1.14 - Added driver_vendor_passthru. 1.15 - Added rebuild parameter to do_node_deploy. + 1.16 - Added get_driver_properties. """ # NOTE(rloo): This must be in sync with manager.ConductorManager's. - RPC_API_VERSION = '1.15' + RPC_API_VERSION = '1.16' def __init__(self, topic=None): super(ConductorAPI, self).__init__() @@ -311,3 +312,18 @@ class ConductorAPI(object): """ cctxt = self.client.prepare(topic=topic or self.topic, version='1.13') return cctxt.call(context, 'update_port', port_obj=port_obj) + + def get_driver_properties(self, context, driver_name, topic=None): + """Get the properties of the driver. + + :param context: request context. + :param driver_name: name of the driver. + :param topic: RPC topic. Defaults to self.topic. + :returns: a dictionary with <property name>:<property description> + entries. + :raises: DriverNotFound. + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.16') + return cctxt.call(context, 'get_driver_properties', + driver_name=driver_name) diff --git a/ironic/tests/api/v1/test_drivers.py b/ironic/tests/api/v1/test_drivers.py index 1d2819c3d..bea09e382 100644 --- a/ironic/tests/api/v1/test_drivers.py +++ b/ironic/tests/api/v1/test_drivers.py @@ -17,6 +17,8 @@ import json import mock from testtools.matchers import HasLength +from ironic.api.controllers.v1 import driver +from ironic.common import exception from ironic.conductor import rpcapi from ironic.tests.api import base @@ -47,11 +49,11 @@ class TestListDrivers(base.FunctionalTest): self.assertThat(data['drivers'], HasLength(2)) drivers = sorted(data['drivers']) for i in range(len(expected)): - driver = drivers[i] - self.assertEqual(expected[i]['name'], driver['name']) - self.assertEqual(expected[i]['hosts'], driver['hosts']) - self.validate_link(driver['links'][0]['href']) - self.validate_link(driver['links'][1]['href']) + d = drivers[i] + self.assertEqual(expected[i]['name'], d['name']) + self.assertEqual(expected[i]['hosts'], d['hosts']) + self.validate_link(d['links'][0]['href']) + self.validate_link(d['links'][1]['href']) def test_drivers_no_active_conductor(self): data = self.get_json('/drivers') @@ -103,3 +105,69 @@ class TestListDrivers(base.FunctionalTest): error = json.loads(response.json['error_message']) self.assertEqual('Missing argument: "method"', error['faultstring']) + + +@mock.patch.object(rpcapi.ConductorAPI, 'get_driver_properties') +@mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for_driver') +class TestDriverProperties(base.FunctionalTest): + + def test_driver_properties_fake(self, mock_topic, mock_properties): + # Can get driver properties for fake driver. + driver._DRIVER_PROPERTIES = {} + driver_name = 'fake' + mock_topic.return_value = 'fake_topic' + mock_properties.return_value = {'prop1': 'Property 1. Required.'} + data = self.get_json('/drivers/%s/properties' % driver_name) + self.assertEqual(mock_properties.return_value, data) + mock_topic.assert_called_once_with(driver_name) + mock_properties.assert_called_once_with(mock.ANY, driver_name, + topic=mock_topic.return_value) + self.assertEqual(mock_properties.return_value, + driver._DRIVER_PROPERTIES[driver_name]) + + def test_driver_properties_cached(self, mock_topic, mock_properties): + # only one RPC-conductor call will be made and the info cached + # for subsequent requests + driver._DRIVER_PROPERTIES = {} + driver_name = 'fake' + mock_topic.return_value = 'fake_topic' + mock_properties.return_value = {'prop1': 'Property 1. Required.'} + data = self.get_json('/drivers/%s/properties' % driver_name) + data = self.get_json('/drivers/%s/properties' % driver_name) + data = self.get_json('/drivers/%s/properties' % driver_name) + self.assertEqual(mock_properties.return_value, data) + mock_topic.assert_called_once_with(driver_name) + mock_properties.assert_called_once_with(mock.ANY, driver_name, + topic=mock_topic.return_value) + self.assertEqual(mock_properties.return_value, + driver._DRIVER_PROPERTIES[driver_name]) + + def test_driver_properties_invalid_driver_name(self, mock_topic, + mock_properties): + # Cannot get driver properties for an invalid driver; no RPC topic + # exists for it. + driver._DRIVER_PROPERTIES = {} + driver_name = 'bad_driver' + mock_topic.side_effect = exception.DriverNotFound( + driver_name=driver_name) + mock_properties.return_value = {'prop1': 'Property 1. Required.'} + ret = self.get_json('/drivers/%s/properties' % driver_name, + expect_errors=True) + self.assertEqual(404, ret.status_int) + mock_topic.assert_called_once_with(driver_name) + self.assertFalse(mock_properties.called) + + def test_driver_properties_cannot_load(self, mock_topic, mock_properties): + # Cannot get driver properties for the driver. Although an RPC topic + # exists for it, the conductor wasn't able to load it. + driver._DRIVER_PROPERTIES = {} + driver_name = 'driver' + mock_topic.return_value = 'driver_topic' + mock_properties.side_effect = exception.DriverNotFound( + driver_name=driver_name) + ret = self.get_json('/drivers/%s/properties' % driver_name, + expect_errors=True) + self.assertEqual(404, ret.status_int) + mock_topic.assert_called_once_with(driver_name) + mock_properties.assert_called_once_with(mock.ANY, driver_name, + topic=mock_topic.return_value) diff --git a/ironic/tests/conductor/test_manager.py b/ironic/tests/conductor/test_manager.py index 2e3c20e3f..8af07b747 100644 --- a/ironic/tests/conductor/test_manager.py +++ b/ironic/tests/conductor/test_manager.py @@ -235,6 +235,16 @@ class ManagerTestCase(tests_db_base.DbTestCase): self.service._conductor_service_record_keepalive() mock_touch.assert_called_once_with(self.hostname) + def test_get_driver_known(self): + self._start_service() + driver = self.service._get_driver('fake') + self.assertTrue(isinstance(driver, drivers_base.BaseDriver)) + + def test_get_driver_unknown(self): + self._start_service() + self.assertRaises(exception.DriverNotFound, + self.service._get_driver, 'unknown_driver') + def test_change_node_power_state_power_on(self): # Test change_node_power_state including integration with # conductor.utils.node_power_action and lower. @@ -1858,3 +1868,89 @@ class ManagerCheckDeployTimeoutsTestCase(_CommonMixIn, tests_base.TestCase): self.task) self.assertEqual([spawn_after_call] * 2, self.task.spawn_after.call_args_list) + + +class ManagerTestProperties(tests_db_base.DbTestCase): + + def setUp(self): + super(ManagerTestProperties, self).setUp() + self.service = manager.ConductorManager('test-host', 'test-topic') + self.context = context.get_admin_context() + + def _check_driver_properties(self, driver, expected): + mgr_utils.mock_the_extension_manager(driver=driver) + self.driver = driver_factory.get_driver(driver) + self.service.init_host() + properties = self.service.get_driver_properties(self.context, driver) + self.assertEqual(sorted(expected), sorted(properties.keys())) + + def test_driver_properties_fake(self): + expected = ['A1', 'A2', 'B1', 'B2'] + self._check_driver_properties("fake", expected) + + def test_driver_properties_fake_ipmitool(self): + expected = ['ipmi_address', 'ipmi_terminal_port', + 'ipmi_password', 'ipmi_priv_level', + 'ipmi_username'] + self._check_driver_properties("fake_ipmitool", expected) + + def test_driver_properties_fake_ipminative(self): + expected = ['ipmi_address', 'ipmi_password', 'ipmi_username'] + self._check_driver_properties("fake_ipminative", expected) + + def test_driver_properties_fake_ssh(self): + expected = ['ssh_address', 'ssh_username', 'ssh_virt_type', + 'ssh_key_contents', 'ssh_key_filename', + 'ssh_password', 'ssh_port'] + self._check_driver_properties("fake_ssh", expected) + + def test_driver_properties_fake_pxe(self): + expected = ['pxe_deploy_kernel', 'pxe_deploy_ramdisk'] + self._check_driver_properties("fake_pxe", expected) + + def test_driver_properties_fake_seamicro(self): + expected = ['seamicro_api_endpoint', 'seamicro_password', + 'seamicro_server_id', 'seamicro_username', + 'seamicro_api_version'] + self._check_driver_properties("fake_seamicro", expected) + + def test_driver_properties_pxe_ipmitool(self): + expected = ['ipmi_address', 'ipmi_terminal_port', + 'pxe_deploy_kernel', 'pxe_deploy_ramdisk', + 'ipmi_password', 'ipmi_priv_level', + 'ipmi_username'] + self._check_driver_properties("pxe_ipmitool", expected) + + def test_driver_properties_pxe_ipminative(self): + expected = ['ipmi_address', 'ipmi_password', 'ipmi_username', + 'pxe_deploy_kernel', 'pxe_deploy_ramdisk'] + self._check_driver_properties("pxe_ipminative", expected) + + def test_driver_properties_pxe_ssh(self): + expected = ['pxe_deploy_kernel', 'pxe_deploy_ramdisk', + 'ssh_address', 'ssh_username', 'ssh_virt_type', + 'ssh_key_contents', 'ssh_key_filename', + 'ssh_password', 'ssh_port'] + self._check_driver_properties("pxe_ssh", expected) + + def test_driver_properties_pxe_seamicro(self): + expected = ['pxe_deploy_kernel', 'pxe_deploy_ramdisk', + 'seamicro_api_endpoint', 'seamicro_password', + 'seamicro_server_id', 'seamicro_username', + 'seamicro_api_version'] + self._check_driver_properties("pxe_seamicro", expected) + + def test_driver_properties_ilo(self): + expected = ['ilo_address', 'ilo_username', 'ilo_password', + 'client_port', 'client_timeout'] + self._check_driver_properties("ilo", expected) + + def test_driver_properties_fail(self): + mgr_utils.mock_the_extension_manager() + self.driver = driver_factory.get_driver("fake") + self.service.init_host() + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.get_driver_properties, + self.context, "bad-driver") + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.DriverNotFound, exc.exc_info[0]) diff --git a/ironic/tests/conductor/test_rpcapi.py b/ironic/tests/conductor/test_rpcapi.py index da1580e85..3c6951d4b 100644 --- a/ironic/tests/conductor/test_rpcapi.py +++ b/ironic/tests/conductor/test_rpcapi.py @@ -221,3 +221,8 @@ class RPCAPITestCase(base.DbTestCase): 'call', version='1.13', port_obj=fake_port) + + def test_get_driver_properties(self): + self._test_rpcapi('get_driver_properties', + 'call', + driver_name='fake-driver') |