Make vendor methods discoverable via the Ironic API
The @passthru and @driver_passthru decorators were extended to support adding a description about the method being decorated which will also be returned by the API. That said two new endpoints to the API that returns the available methods for the given driver or node. * GET /v1/drivers/<driver name>/vendor_passthru/methods * GET /v1/nodes/<node uuid>/vendor_passthru/methods Implements: blueprint extended-vendor-passthru Change-Id: Idac6a433da1a9ae170dbe3d8ceb9e4c76fcdb1be
This commit is contained in:
parent
6e2f985b64
commit
e69e53097a
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user