summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2014-07-24 21:40:16 +0000
committerGerrit Code Review <review@openstack.org>2014-07-24 21:40:16 +0000
commitb3e4e4fb7172cd2db04d9a711712da93c6160eef (patch)
tree961a28a4f9cd34576fb669512b85768928fa075b
parentb6741b5adf17233319570b5f9f1950ca0e389905 (diff)
parenta65d3e6b1126b4e148e825290a61b151e1a6559c (diff)
downloadironic-b3e4e4fb7172cd2db04d9a711712da93c6160eef.tar.gz
Merge "Implement API to get driver properties"
-rw-r--r--ironic/api/controllers/v1/driver.py33
-rw-r--r--ironic/common/hash_ring.py3
-rw-r--r--ironic/conductor/manager.py43
-rw-r--r--ironic/conductor/rpcapi.py18
-rw-r--r--ironic/tests/api/v1/test_drivers.py78
-rw-r--r--ironic/tests/conductor/test_manager.py96
-rw-r--r--ironic/tests/conductor/test_rpcapi.py5
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 d8162525d..30b107e1b 100644
--- a/ironic/conductor/manager.py
+++ b/ironic/conductor/manager.py
@@ -130,7 +130,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)
@@ -141,11 +141,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:
@@ -326,11 +341,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,
@@ -966,3 +977,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')