Move wait_for_baremetal_node_lock to the baremetal proxy

It is a quite low-level method that regular users should not use. This patch
deprecates it in favor of new baremetal.wait_for_node_reservation.

Change-Id: I071bdea02898cf911c2c018d014895c8a856524c
This commit is contained in:
Dmitry Tantsur 2018-10-16 14:52:19 +02:00
parent c16cc13fd8
commit 17230af4ff
7 changed files with 121 additions and 40 deletions

View File

@ -24,6 +24,7 @@ Node Operations
.. automethod:: openstack.baremetal.v1._proxy.Proxy.nodes
.. automethod:: openstack.baremetal.v1._proxy.Proxy.set_node_provision_state
.. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_nodes_provision_state
.. automethod:: openstack.baremetal.v1._proxy.Proxy.wait_for_node_reservation
.. automethod:: openstack.baremetal.v1._proxy.Proxy.validate_node
Port Operations

View File

@ -332,6 +332,32 @@ class Proxy(proxy.Proxy):
{'nodes': ', '.join(n.id for n in remaining),
'target': expected_state})
def wait_for_node_reservation(self, node, timeout=None):
"""Wait for a lock on the node to be released.
Bare metal nodes in ironic have a reservation lock that
is used to represent that a conductor has locked the node
while performing some sort of action, such as changing
configuration as a result of a machine state change.
This lock can occur during power syncronization, and prevents
updates to objects attached to the node, such as ports.
Note that nothing prevents a conductor from acquiring the lock again
after this call returns, so it should be treated as best effort.
Returns immediately if there is no reservation on the node.
:param node: The value can be the name or ID of a node or a
:class:`~openstack.baremetal.v1.node.Node` instance.
:param timeout: How much (in seconds) to wait for the lock to be
released. The value of ``None`` (the default) means no timeout.
:returns: The updated :class:`~openstack.baremetal.v1.node.Node`
"""
res = self._get_resource(_node.Node, node)
return res.wait_for_reservation(self, timeout=timeout)
def validate_node(self, node, required=('boot', 'deploy', 'power')):
"""Validate required information on a node.

View File

@ -367,6 +367,44 @@ class Node(resource.Resource):
{'node': self.id, 'target': expected_state,
'state': self.provision_state})
def wait_for_reservation(self, session, timeout=None):
"""Wait for a lock on the node to be released.
Bare metal nodes in ironic have a reservation lock that
is used to represent that a conductor has locked the node
while performing some sort of action, such as changing
configuration as a result of a machine state change.
This lock can occur during power syncronization, and prevents
updates to objects attached to the node, such as ports.
Note that nothing prevents a conductor from acquiring the lock again
after this call returns, so it should be treated as best effort.
Returns immediately if there is no reservation on the node.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param timeout: How much (in seconds) to wait for the lock to be
released. The value of ``None`` (the default) means no timeout.
:return: This :class:`Node` instance.
"""
if self.reservation is None:
return self
for count in utils.iterate_timeout(
timeout,
"Timeout waiting for the lock to be released on node %s" %
self.id):
self.fetch(session)
if self.reservation is None:
return self
_logger.debug('Still waiting for the lock to be released on node '
'%(node)s, currently locked by conductor %(host)s',
{'node': self.id, 'host': self.reservation})
def _check_state_reached(self, session, expected_state,
abort_on_failed_state=True):
"""Wait for the node to reach the expected state.

View File

@ -10149,49 +10149,15 @@ class OpenStackCloud(_normalize.Normalizer):
def wait_for_baremetal_node_lock(self, node, timeout=30):
"""Wait for a baremetal node to have no lock.
Baremetal nodes in ironic have a reservation lock that
is used to represent that a conductor has locked the node
while performing some sort of action, such as changing
configuration as a result of a machine state change.
This lock can occur during power syncronization, and prevents
updates to objects attached to the node, such as ports.
In the vast majority of cases, locks should clear in a few
seconds, and as such this method will only wait for 30 seconds.
The default wait is two seconds between checking if the lock
has cleared.
This method is intended for use by methods that need to
gracefully block without genreating errors, however this
method does prevent another client or a timer from
triggering a lock immediately after we see the lock as
having cleared.
:param node: The json representation of the node,
specificially looking for the node
'uuid' and 'reservation' fields.
:param timeout: Integer in seconds to wait for the
lock to clear. Default: 30
DEPRECATED, use ``wait_for_node_reservation`` on the `baremetal` proxy.
:raises: OpenStackCloudException upon client failure.
:returns: None
"""
# TODO(TheJulia): This _can_ still fail with a race
# condition in that between us checking the status,
# a conductor where the conductor could still obtain
# a lock before we are able to obtain a lock.
# This means we should handle this with such conections
if node['reservation'] is None:
return
else:
msg = 'Waiting for lock to be released for node {node}'.format(
node=node['uuid'])
for count in utils.iterate_timeout(timeout, msg, 2):
current_node = self.get_machine(node['uuid'])
if current_node['reservation'] is None:
return
warnings.warn("The wait_for_baremetal_node_lock call is deprecated "
"in favor of wait_for_node_reservation on the baremetal "
"proxy", DeprecationWarning)
self.baremetal.wait_for_node_reservation(node, timeout)
@_utils.valid_kwargs('type', 'service_type', 'description')
def create_service(self, name, enabled=True, **kwargs):

View File

@ -492,3 +492,42 @@ class TestNodeValidate(base.TestCase):
# Reason can be empty
self.assertFalse(result['boot'].result)
self.assertIsNone(result['boot'].reason)
@mock.patch('time.sleep', lambda _t: None)
@mock.patch.object(node.Node, 'fetch', autospec=True)
class TestNodeWaitForReservation(base.TestCase):
def setUp(self):
super(TestNodeWaitForReservation, self).setUp()
self.session = mock.Mock(spec=adapter.Adapter)
self.session.default_microversion = '1.6'
self.node = node.Node(**FAKE)
def test_no_reservation(self, mock_fetch):
self.node.reservation = None
node = self.node.wait_for_reservation(None)
self.assertIs(node, self.node)
self.assertFalse(mock_fetch.called)
def test_reservation(self, mock_fetch):
self.node.reservation = 'example.com'
def _side_effect(node, session):
if self.node.reservation == 'example.com':
self.node.reservation = 'example2.com'
else:
self.node.reservation = None
mock_fetch.side_effect = _side_effect
node = self.node.wait_for_reservation(self.session)
self.assertIs(node, self.node)
self.assertEqual(2, mock_fetch.call_count)
def test_timeout(self, mock_fetch):
self.node.reservation = 'example.com'
self.assertRaises(exceptions.ResourceTimeout,
self.node.wait_for_reservation,
self.session, timeout=0.001)
mock_fetch.assert_called_with(self.node, self.session)

View File

@ -912,7 +912,8 @@ class TestBaremetalNode(base.IronicTestCase):
self.fake_baremetal_node,
timeout=1))
self.assertEqual(0, len(self.adapter.request_history))
# NOTE(dtantsur): service discovery apparently requires 3 calls
self.assertEqual(3, len(self.adapter.request_history))
def test_wait_for_baremetal_node_lock_timeout(self):
self.fake_baremetal_node['reservation'] = 'conductor0'

View File

@ -0,0 +1,10 @@
---
features:
- |
Added ``wait_for_node_reservation`` to the baremetal proxy.
deprecations:
- |
The `OpenStackCloud` ``wait_for_baremetal_node_lock`` call is deprecated.
Generally, users should not have to call it. The new
``wait_for_node_reservation`` from the baremetal proxy can be used when
needed.