Merge "Make vendor methods discoverable via the Ironic API"
This commit is contained in:
commit
6e91483fa5
@ -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