Merge "Allow vendor drivers to acquire shared locks"
This commit is contained in:
commit
7c855d5868
@ -53,9 +53,10 @@ A method:
|
||||
+ For synchronous methods, a 200 (OK) HTTP status code is returned to
|
||||
indicate that the request was fulfilled. The response may include a body.
|
||||
|
||||
While performing the request, a lock is held on the node, and other
|
||||
requests for the node will be delayed and may fail with an HTTP 409
|
||||
(Conflict) error code.
|
||||
* can require an exclusive lock on the node. This only occurs if the method
|
||||
doesn't specify require_exclusive_lock=False in the decorator. If an
|
||||
exclusive lock is held on the node, other requests for the node will be
|
||||
delayed and may fail with an HTTP 409 (Conflict) error code.
|
||||
|
||||
This endpoint exposes a node's driver directly, and as such, it is
|
||||
expressly not part of Ironic's standard REST API. There is only a
|
||||
|
@ -95,7 +95,7 @@ parameter of the method (ignoring self). A method decorated with the
|
||||
a method decorated with the `@driver_passthru` decorator should expect
|
||||
a Context object as first parameter.
|
||||
|
||||
Both decorators accepts the same parameters:
|
||||
Both decorators accept these parameters:
|
||||
|
||||
* http_methods: A list of what the HTTP methods supported by that vendor
|
||||
function. To know what HTTP method that function was invoked with, a
|
||||
@ -120,6 +120,14 @@ Both decorators accepts the same parameters:
|
||||
* async: A boolean value to determine whether this method should run
|
||||
asynchronously or synchronously. Defaults to True (Asynchronously).
|
||||
|
||||
The node vendor passthru decorator (`@passthru`) also accepts the following
|
||||
parameter:
|
||||
|
||||
* require_exclusive_lock: A boolean value determining whether this method
|
||||
should require an exclusive lock on a node between validate() and the
|
||||
beginning of method execution. For synchronous methods, the lock on the node
|
||||
would also be kept for the duration of method execution. Defaults to True.
|
||||
|
||||
.. WARNING::
|
||||
Please avoid having a synchronous method for slow/long-running
|
||||
operations **or** if the method does talk to a BMC; BMCs are flaky
|
||||
|
@ -279,10 +279,9 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
http_method, info):
|
||||
"""RPC method to encapsulate vendor action.
|
||||
|
||||
Synchronously validate driver specific info or get driver status,
|
||||
and if successful invokes the vendor method. If the method mode
|
||||
is 'async' the conductor will start background worker to perform
|
||||
vendor action.
|
||||
Synchronously validate driver specific info, and if successful invoke
|
||||
the vendor method. If the method mode is 'async' the conductor will
|
||||
start background worker to perform vendor action.
|
||||
|
||||
:param context: an admin context.
|
||||
:param node_id: the id or uuid of a node.
|
||||
@ -295,7 +294,8 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
vendor interface or method is unsupported.
|
||||
:raises: NoFreeConductorWorker when there is no free worker to start
|
||||
async task.
|
||||
:raises: NodeLocked if node is locked by another conductor.
|
||||
:raises: NodeLocked if the vendor passthru method requires an exclusive
|
||||
lock but the node is locked by another conductor
|
||||
:returns: A dictionary containing:
|
||||
|
||||
:return: The response of the invoked vendor method
|
||||
@ -308,11 +308,11 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
|
||||
"""
|
||||
LOG.debug("RPC vendor_passthru called for node %s." % node_id)
|
||||
# NOTE(max_lobur): Even though not all vendor_passthru calls may
|
||||
# require an exclusive lock, we need to do so to guarantee that the
|
||||
# state doesn't unexpectedly change between doing a vendor.validate
|
||||
# and vendor.vendor_passthru.
|
||||
with task_manager.acquire(context, node_id, shared=False,
|
||||
# NOTE(mariojv): Not all vendor passthru methods require an exclusive
|
||||
# lock on a node, so we acquire a shared lock initially. If a method
|
||||
# requires an exclusive lock, we'll acquire one after checking
|
||||
# vendor_opts before starting validation.
|
||||
with task_manager.acquire(context, node_id, shared=True,
|
||||
purpose='calling vendor passthru') as task:
|
||||
if not getattr(task.driver, 'vendor', None):
|
||||
raise exception.UnsupportedDriverExtension(
|
||||
@ -334,6 +334,11 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
_('The method %(method)s does not support HTTP %(http)s') %
|
||||
{'method': driver_method, 'http': http_method})
|
||||
|
||||
# Change shared lock to exclusive if a vendor method requires
|
||||
# it. Vendor methods default to requiring an exclusive lock.
|
||||
if vendor_opts['require_exclusive_lock']:
|
||||
task.upgrade_lock()
|
||||
|
||||
vendor_iface.validate(task, method=driver_method,
|
||||
http_method=http_method, **info)
|
||||
|
||||
|
@ -258,6 +258,9 @@ class TaskManager(object):
|
||||
|
||||
Also reloads node object from the database.
|
||||
Does nothing if lock is already exclusive.
|
||||
|
||||
:raises: NodeLocked if an exclusive lock remains on the node after
|
||||
"node_locked_retry_attempts"
|
||||
"""
|
||||
if self.shared:
|
||||
LOG.debug('Upgrading shared lock on node %(uuid)s for %(purpose)s '
|
||||
|
@ -611,7 +611,7 @@ VendorMetadata = collections.namedtuple('VendorMetadata', ['method',
|
||||
|
||||
|
||||
def _passthru(http_methods, method=None, async=True, driver_passthru=False,
|
||||
description=None, attach=False):
|
||||
description=None, attach=False, require_exclusive_lock=True):
|
||||
"""A decorator for registering a function as a passthru function.
|
||||
|
||||
Decorator ensures function is ready to catch any ironic exceptions
|
||||
@ -637,7 +637,12 @@ def _passthru(http_methods, method=None, async=True, driver_passthru=False,
|
||||
value should be returned in the response body.
|
||||
Defaults to False.
|
||||
:param description: a string shortly describing what the method does.
|
||||
|
||||
:param require_exclusive_lock: Boolean value. Only valid for node passthru
|
||||
methods. If True, lock the node before
|
||||
validate() and invoking the vendor method.
|
||||
The node remains locked during execution
|
||||
for a synchronous passthru method. If False,
|
||||
don't lock the node. Defaults to True.
|
||||
"""
|
||||
def handle_passthru(func):
|
||||
api_method = method
|
||||
@ -653,6 +658,7 @@ def _passthru(http_methods, method=None, async=True, driver_passthru=False,
|
||||
if driver_passthru:
|
||||
func._driver_metadata = metadata
|
||||
else:
|
||||
metadata[1]['require_exclusive_lock'] = require_exclusive_lock
|
||||
func._vendor_metadata = metadata
|
||||
|
||||
passthru_logmessage = _LE('vendor_passthru failed with method %s')
|
||||
@ -673,9 +679,10 @@ def _passthru(http_methods, method=None, async=True, driver_passthru=False,
|
||||
|
||||
|
||||
def passthru(http_methods, method=None, async=True, description=None,
|
||||
attach=False):
|
||||
attach=False, require_exclusive_lock=True):
|
||||
return _passthru(http_methods, method, async, driver_passthru=False,
|
||||
description=description, attach=attach)
|
||||
description=description, attach=attach,
|
||||
require_exclusive_lock=require_exclusive_lock)
|
||||
|
||||
|
||||
def driver_passthru(http_methods, method=None, async=True, description=None,
|
||||
|
@ -70,7 +70,8 @@ class FakeDriver(base.BaseDriver):
|
||||
self.b = fake.FakeVendorB()
|
||||
self.mapping = {'first_method': self.a,
|
||||
'second_method': self.b,
|
||||
'third_method_sync': self.b}
|
||||
'third_method_sync': self.b,
|
||||
'fourth_method_shared_lock': self.b}
|
||||
self.vendor = utils.MixinVendorInterface(self.mapping)
|
||||
self.console = fake.FakeConsole()
|
||||
self.management = fake.FakeManagement()
|
||||
|
@ -133,7 +133,8 @@ class FakeVendorB(base.VendorInterface):
|
||||
'B2': 'B2 description. Required.'}
|
||||
|
||||
def validate(self, task, method, **kwargs):
|
||||
if method in ('second_method', 'third_method_sync'):
|
||||
if method in ('second_method', 'third_method_sync',
|
||||
'fourth_method_shared_lock'):
|
||||
bar = kwargs.get('bar')
|
||||
if not bar:
|
||||
raise exception.MissingParameterValue(_(
|
||||
@ -149,6 +150,11 @@ class FakeVendorB(base.VendorInterface):
|
||||
def third_method_sync(self, task, http_method, bar):
|
||||
return True if bar == 'meow' else False
|
||||
|
||||
@base.passthru(['POST'], require_exclusive_lock=False,
|
||||
description=_("Test if the value of bar is woof"))
|
||||
def fourth_method_shared_lock(self, task, http_method, bar):
|
||||
return True if bar == 'woof' else False
|
||||
|
||||
|
||||
class FakeConsole(base.ConsoleInterface):
|
||||
"""Example implementation of a simple console interface."""
|
||||
|
@ -290,8 +290,9 @@ class UpdateNodeTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
tests_db_base.DbTestCase):
|
||||
|
||||
@mock.patch.object(task_manager.TaskManager, 'upgrade_lock')
|
||||
@mock.patch.object(task_manager.TaskManager, 'spawn_after')
|
||||
def test_vendor_passthru_async(self, mock_spawn):
|
||||
def test_vendor_passthru_async(self, mock_spawn, mock_upgrade):
|
||||
node = obj_utils.create_test_node(self.context, driver='fake')
|
||||
info = {'bar': 'baz'}
|
||||
self._start_service()
|
||||
@ -307,13 +308,17 @@ class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
self.assertIsNone(response['return'])
|
||||
self.assertTrue(response['async'])
|
||||
|
||||
# Assert lock was upgraded to an exclusive one
|
||||
self.assertEqual(1, mock_upgrade.call_count)
|
||||
|
||||
node.refresh()
|
||||
self.assertIsNone(node.last_error)
|
||||
# Verify reservation has been cleared.
|
||||
self.assertIsNone(node.reservation)
|
||||
|
||||
@mock.patch.object(task_manager.TaskManager, 'upgrade_lock')
|
||||
@mock.patch.object(task_manager.TaskManager, 'spawn_after')
|
||||
def test_vendor_passthru_sync(self, mock_spawn):
|
||||
def test_vendor_passthru_sync(self, mock_spawn, mock_upgrade):
|
||||
node = obj_utils.create_test_node(self.context, driver='fake')
|
||||
info = {'bar': 'meow'}
|
||||
self._start_service()
|
||||
@ -329,11 +334,40 @@ class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
self.assertTrue(response['return'])
|
||||
self.assertFalse(response['async'])
|
||||
|
||||
# Assert lock was upgraded to an exclusive one
|
||||
self.assertEqual(1, mock_upgrade.call_count)
|
||||
|
||||
node.refresh()
|
||||
self.assertIsNone(node.last_error)
|
||||
# Verify reservation has been cleared.
|
||||
self.assertIsNone(node.reservation)
|
||||
|
||||
@mock.patch.object(task_manager.TaskManager, 'upgrade_lock')
|
||||
@mock.patch.object(task_manager.TaskManager, 'spawn_after')
|
||||
def test_vendor_passthru_shared_lock(self, mock_spawn, mock_upgrade):
|
||||
node = obj_utils.create_test_node(self.context, driver='fake')
|
||||
info = {'bar': 'woof'}
|
||||
self._start_service()
|
||||
|
||||
response = self.service.vendor_passthru(self.context, node.uuid,
|
||||
'fourth_method_shared_lock',
|
||||
'POST', info)
|
||||
# Waiting to make sure the below assertions are valid.
|
||||
self._stop_service()
|
||||
|
||||
# Assert spawn_after was called
|
||||
self.assertTrue(mock_spawn.called)
|
||||
self.assertIsNone(response['return'])
|
||||
self.assertTrue(response['async'])
|
||||
|
||||
# Assert lock was never upgraded to an exclusive one
|
||||
self.assertFalse(mock_upgrade.called)
|
||||
|
||||
node.refresh()
|
||||
self.assertIsNone(node.last_error)
|
||||
# Verify there's no reservation on the node
|
||||
self.assertIsNone(node.reservation)
|
||||
|
||||
def test_vendor_passthru_http_method_not_supported(self):
|
||||
node = obj_utils.create_test_node(self.context, driver='fake')
|
||||
self._start_service()
|
||||
|
@ -44,6 +44,10 @@ class FakeVendorInterface(driver_base.VendorInterface):
|
||||
def normalexception(self):
|
||||
raise Exception("Fake!")
|
||||
|
||||
@driver_base.passthru(['POST'], require_exclusive_lock=False)
|
||||
def shared_task(self):
|
||||
return "shared fake"
|
||||
|
||||
def validate(self, task, **kwargs):
|
||||
pass
|
||||
|
||||
@ -75,6 +79,18 @@ class PassthruDecoratorTestCase(base.TestCase):
|
||||
mock_log.exception.assert_called_with(
|
||||
mock.ANY, 'normalexception')
|
||||
|
||||
def test_passthru_shared_task_metadata(self):
|
||||
self.assertIn('require_exclusive_lock',
|
||||
self.fvi.shared_task._vendor_metadata[1])
|
||||
self.assertFalse(
|
||||
self.fvi.shared_task._vendor_metadata[1]['require_exclusive_lock'])
|
||||
|
||||
def test_passthru_exclusive_task_metadata(self):
|
||||
self.assertIn('require_exclusive_lock',
|
||||
self.fvi.noexception._vendor_metadata[1])
|
||||
self.assertTrue(
|
||||
self.fvi.noexception._vendor_metadata[1]['require_exclusive_lock'])
|
||||
|
||||
def test_passthru_check_func_references(self):
|
||||
inst1 = FakeVendorInterface()
|
||||
inst2 = FakeVendorInterface()
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Adds the ability for node vendor passthru methods to use shared locks.
|
||||
Default behavior of always acquiring an exclusive lock for node vendor
|
||||
passthru methods is unchanged.
|
Loading…
x
Reference in New Issue
Block a user