Merge "Make vendor methods discoverable via the Ironic API"

This commit is contained in:
Jenkins 2014-11-19 13:58:13 +00:00 committed by Gerrit Code Review
commit 6e91483fa5
10 changed files with 273 additions and 14 deletions

View File

@ -35,6 +35,16 @@ from ironic.common.i18n import _
# service should be restarted.
_DRIVER_PROPERTIES = {}
# Vendor information for drivers:
# key = driver name;
# value = dictionary of vendor methods of that driver:
# key = method name.
# value = dictionary with the metadata of that method.
# NOTE(lucasagomes). 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.
_VENDOR_METHODS = {}
class Driver(base.APIBase):
"""API representation of a driver."""
@ -100,6 +110,28 @@ class DriverPassthruController(rest.RestController):
driver, no introspection will be made in the message body.
"""
_custom_actions = {
'methods': ['GET']
}
@wsme_pecan.wsexpose(wtypes.text, wtypes.text)
def methods(self, driver_name):
"""Retrieve information about vendor methods of the given driver.
:param driver_name: name of the driver.
:returns: dictionary with <vendor method name>:<method metadata>
entries.
:raises: DriverNotFound if the driver name is invalid or the
driver cannot be loaded.
"""
if driver_name not in _VENDOR_METHODS:
topic = pecan.request.rpcapi.get_topic_for_driver(driver_name)
ret = pecan.request.rpcapi.get_driver_vendor_passthru_methods(
pecan.request.context, driver_name, topic=topic)
_VENDOR_METHODS[driver_name] = ret
return _VENDOR_METHODS[driver_name]
@wsme_pecan.wsexpose(wtypes.text, wtypes.text, wtypes.text,
body=wtypes.text)
def _default(self, driver_name, method, data=None):

View File

@ -44,6 +44,16 @@ CONF.import_opt('heartbeat_timeout', 'ironic.conductor.manager',
LOG = log.getLogger(__name__)
# Vendor information for node's driver:
# key = driver name;
# value = dictionary of node vendor methods of that driver:
# key = method name.
# value = dictionary with the metadata of that method.
# NOTE(lucasagomes). 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.
_VENDOR_METHODS = {}
class NodePatchType(types.JsonPatchType):
@ -552,6 +562,31 @@ class NodeVendorPassthruController(rest.RestController):
appropriate driver, no introspection will be made in the message body.
"""
_custom_actions = {
'methods': ['GET']
}
@wsme_pecan.wsexpose(wtypes.text, types.uuid)
def methods(self, node_uuid):
"""Retrieve information about vendor methods of the given node.
:param node_uuid: UUID of a node.
:returns: dictionary with <vendor method name>:<method metadata>
entries.
:raises: NodeNotFound if the node is not found.
"""
# Raise an exception if node is not found
rpc_node = objects.Node.get_by_uuid(pecan.request.context,
node_uuid)
if rpc_node.driver not in _VENDOR_METHODS:
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
ret = pecan.request.rpcapi.get_node_vendor_passthru_methods(
pecan.request.context, node_uuid, topic=topic)
_VENDOR_METHODS[rpc_node.driver] = ret
return _VENDOR_METHODS[rpc_node.driver]
@wsme_pecan.wsexpose(wtypes.text, types.uuid, wtypes.text,
body=wtypes.text)
def _default(self, node_uuid, method, data=None):

View File

@ -162,7 +162,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.20'
RPC_API_VERSION = '1.21'
target = messaging.Target(version=RPC_API_VERSION)
@ -398,7 +398,7 @@ class ConductorManager(periodic_task.PeriodicTasks):
if not getattr(task.driver, 'vendor', None):
raise exception.UnsupportedDriverExtension(
driver=task.node.driver,
extension='vendor passthru')
extension='vendor interface')
vendor_iface = task.driver.vendor
@ -544,6 +544,55 @@ class ConductorManager(periodic_task.PeriodicTasks):
return (ret, is_async)
def _get_vendor_passthru_metadata(self, route_dict):
d = {}
for method, metadata in route_dict.iteritems():
# 'func' is the vendor method reference, ignore it
d[method] = {k: metadata[k] for k in metadata if k != 'func'}
return d
@messaging.expected_exceptions(exception.UnsupportedDriverExtension)
def get_node_vendor_passthru_methods(self, context, node_id):
"""Retrieve information about vendor methods of the given node.
:param context: an admin context.
:param node_id: the id or uuid of a node.
:returns: dictionary of <method name>:<method metadata> entries.
"""
LOG.debug("RPC get_node_vendor_passthru_methods called for node %s"
% node_id)
with task_manager.acquire(context, node_id, shared=True) as task:
if not getattr(task.driver, 'vendor', None):
raise exception.UnsupportedDriverExtension(
driver=task.node.driver,
extension='vendor interface')
return self._get_vendor_passthru_metadata(
task.driver.vendor.vendor_routes)
@messaging.expected_exceptions(exception.UnsupportedDriverExtension,
exception.DriverNotFound)
def get_driver_vendor_passthru_methods(self, context, driver_name):
"""Retrieve information about vendor methods of the given driver.
:param context: an admin context.
:param driver_name: name of the driver.
:returns: dictionary of <method name>:<method metadata> entries.
"""
# 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 get_driver_vendor_passthru_methods for driver %s"
% driver_name)
driver = self._get_driver(driver_name)
if not getattr(driver, 'vendor', None):
raise exception.UnsupportedDriverExtension(
driver=driver_name,
extension='vendor interface')
return self._get_vendor_passthru_metadata(driver.vendor.driver_routes)
def _provisioning_error_handler(self, e, node, provision_state,
target_provision_state):
"""Set the node's provisioning states if error occurs.

View File

@ -59,14 +59,16 @@ class ConductorAPI(object):
| get_supported_boot_devices.
| 1.18 - Remove change_node_maintenance_mode.
| 1.19 - Change return value of vendor_passthru and
driver_vendor_passthru
| driver_vendor_passthru
| 1.20 - Added http_method parameter to vendor_passthru and
driver_vendor_passthru
| driver_vendor_passthru
| 1.21 - Added get_node_vendor_passthru_methods and
| get_driver_vendor_passthru_methods
"""
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
RPC_API_VERSION = '1.20'
RPC_API_VERSION = '1.21'
def __init__(self, topic=None):
super(ConductorAPI, self).__init__()
@ -231,6 +233,33 @@ class ConductorAPI(object):
http_method=http_method,
info=info)
def get_node_vendor_passthru_methods(self, context, node_id, topic=None):
"""Retrieve information about vendor methods of the given node.
:param context: an admin context.
:param node_id: the id or uuid of a node.
:param topic: RPC topic. Defaults to self.topic.
:returns: dictionary of <method name>:<method metadata> entries.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.21')
return cctxt.call(context, 'get_node_vendor_passthru_methods',
node_id=node_id)
def get_driver_vendor_passthru_methods(self, context, driver_name,
topic=None):
"""Retrieve information about vendor methods of the given driver.
:param context: an admin context.
:param driver_name: name of the driver.
:param topic: RPC topic. Defaults to self.topic.
:returns: dictionary of <method name>:<method metadata> entries.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.21')
return cctxt.call(context, 'get_driver_vendor_passthru_methods',
driver_name=driver_name)
def do_node_deploy(self, context, node_id, rebuild, topic=None):
"""Signal to conductor service to perform a deployment.

View File

@ -361,7 +361,8 @@ VendorMetadata = collections.namedtuple('VendorMetadata', ['method',
'metadata'])
def _passthru(http_methods, method=None, async=True, driver_passthru=False):
def _passthru(http_methods, method=None, async=True, driver_passthru=False,
description=None):
"""A decorator for registering a function as a passthru function.
Decorator ensures function is ready to catch any ironic exceptions
@ -382,6 +383,7 @@ def _passthru(http_methods, method=None, async=True, driver_passthru=False):
:param driver_passthru: Boolean value. True if this is a driver vendor
passthru method, and False if it is a node
vendor passthru method.
:param description: a string shortly describing what the method does.
"""
def handle_passthru(func):
@ -390,8 +392,10 @@ def _passthru(http_methods, method=None, async=True, driver_passthru=False):
api_method = func.__name__
supported_ = [i.upper() for i in http_methods]
description_ = description or ''
metadata = VendorMetadata(api_method, {'http_methods': supported_,
'async': async})
'async': async,
'description': description_})
if driver_passthru:
func._driver_metadata = metadata
else:
@ -414,12 +418,14 @@ def _passthru(http_methods, method=None, async=True, driver_passthru=False):
return handle_passthru
def passthru(http_methods, method=None, async=True):
return _passthru(http_methods, method, async, driver_passthru=False)
def passthru(http_methods, method=None, async=True, description=None):
return _passthru(http_methods, method, async, driver_passthru=False,
description=description)
def driver_passthru(http_methods, method=None, async=True):
return _passthru(http_methods, method, async, driver_passthru=True)
def driver_passthru(http_methods, method=None, async=True, description=None):
return _passthru(http_methods, method, async, driver_passthru=True,
description=description)
@six.add_metaclass(abc.ABCMeta)

View File

@ -108,7 +108,8 @@ class FakeVendorA(base.VendorInterface):
return
_raise_unsupported_error(method)
@base.passthru(['POST'])
@base.passthru(['POST'],
description=_("Test if the value of bar is baz"))
def first_method(self, task, http_method, bar):
return True if bar == 'baz' else False
@ -130,11 +131,13 @@ class FakeVendorB(base.VendorInterface):
return
_raise_unsupported_error(method)
@base.passthru(['POST'])
@base.passthru(['POST'],
description=_("Test if the value of bar is kazoo"))
def second_method(self, task, http_method, bar):
return True if bar == 'kazoo' else False
@base.passthru(['POST'], async=False)
@base.passthru(['POST'], async=False,
description=_("Test if the value of bar is meow"))
def third_method_sync(self, task, http_method, bar):
return True if bar == 'meow' else False

View File

@ -147,6 +147,28 @@ class TestListDrivers(base.FunctionalTest):
self.assertEqual('Missing argument: "method"',
error['faultstring'])
@mock.patch.object(rpcapi.ConductorAPI,
'get_driver_vendor_passthru_methods')
def test_driver_vendor_passthru_methods(self, get_methods_mock):
self.register_fake_conductors()
return_value = {'foo': 'bar'}
get_methods_mock.return_value = return_value
path = '/drivers/%s/vendor_passthru/methods' % self.d1
data = self.get_json(path)
self.assertEqual(return_value, data)
get_methods_mock.assert_called_once_with(mock.ANY, self.d1,
topic=mock.ANY)
# Now let's test the cache: Reset the mock
get_methods_mock.reset_mock()
# Call it again
data = self.get_json(path)
self.assertEqual(return_value, data)
# Assert RPC method wasn't called this time
self.assertFalse(get_methods_mock.called)
@mock.patch.object(rpcapi.ConductorAPI, 'get_driver_properties')
@mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for_driver')

View File

@ -968,6 +968,27 @@ class TestPost(api_base.FunctionalTest):
self.assertEqual(400, response.status_int)
self.assertTrue(response.json['error_message'])
@mock.patch.object(rpcapi.ConductorAPI, 'get_node_vendor_passthru_methods')
def test_vendor_passthru_methods(self, get_methods_mock):
return_value = {'foo': 'bar'}
get_methods_mock.return_value = return_value
node = obj_utils.create_test_node(self.context)
path = '/nodes/%s/vendor_passthru/methods' % node.uuid
data = self.get_json(path)
self.assertEqual(return_value, data)
get_methods_mock.assert_called_once_with(mock.ANY, node.uuid,
topic=mock.ANY)
# Now let's test the cache: Reset the mock
get_methods_mock.reset_mock()
# Call it again
data = self.get_json(path)
self.assertEqual(return_value, data)
# Assert RPC method wasn't called this time
self.assertFalse(get_methods_mock.called)
class TestDelete(api_base.FunctionalTest):

View File

@ -676,6 +676,31 @@ class VendorPassthruTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase):
task.spawn_after.assert_called_once_with(mock.ANY, vendor_passthru_ref,
task, bar='baz', method='test_method')
def test_get_node_vendor_passthru_methods(self):
node = obj_utils.create_test_node(self.context, driver='fake')
fake_routes = {'test_method': {'async': True,
'description': 'foo',
'http_methods': ['POST'],
'func': None}}
self.driver.vendor.vendor_routes = fake_routes
self._start_service()
data = self.service.get_node_vendor_passthru_methods(self.context,
node.uuid)
# The function reference should not be returned
del fake_routes['test_method']['func']
self.assertEqual(fake_routes, data)
def test_get_node_vendor_passthru_methods_not_supported(self):
node = obj_utils.create_test_node(self.context, driver='fake')
self.driver.vendor = None
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.get_node_vendor_passthru_methods,
self.context, node.uuid)
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.UnsupportedDriverExtension,
exc.exc_info[0])
@mock.patch.object(manager.ConductorManager, '_spawn_worker')
def test_driver_vendor_passthru_sync(self, mock_spawn):
expected = {'foo': 'bar'}
@ -791,6 +816,31 @@ class VendorPassthruTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase):
driver_vendor_passthru_ref.assert_called_once_with(
self.context, test='arg', method='test_method')
def test_get_driver_vendor_passthru_methods(self):
self.driver.vendor = mock.Mock(spec=drivers_base.VendorInterface)
fake_routes = {'test_method': {'async': True,
'description': 'foo',
'http_methods': ['POST'],
'func': None}}
self.driver.vendor.driver_routes = fake_routes
self.service.init_host()
data = self.service.get_driver_vendor_passthru_methods(self.context,
'fake')
# The function reference should not be returned
del fake_routes['test_method']['func']
self.assertEqual(fake_routes, data)
def test_get_driver_vendor_passthru_methods_not_supported(self):
self.service.init_host()
self.driver.vendor = None
exc = self.assertRaises(messaging.rpc.ExpectedException,
self.service.get_driver_vendor_passthru_methods,
self.context, 'fake')
# Compare true exception hidden by @messaging.expected_exceptions
self.assertEqual(exception.UnsupportedDriverExtension,
exc.exc_info[0])
@_mock_record_keepalive
class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin,

View File

@ -269,3 +269,15 @@ class RPCAPITestCase(base.DbTestCase):
'call',
version='1.17',
node_id=self.fake_node['uuid'])
def test_get_node_vendor_passthru_methods(self):
self._test_rpcapi('get_node_vendor_passthru_methods',
'call',
version='1.21',
node_id=self.fake_node['uuid'])
def test_get_driver_vendor_passthru_methods(self):
self._test_rpcapi('get_driver_vendor_passthru_methods',
'call',
version='1.21',
driver_name='fake-driver')