Add reset_interfaces argument to patch_node

Ironic uses reset_interfaces option when patching a node to
reset all the hardware interfaces of a node.
This patch adds that argument to the patch_node method in
the baremetal module and introduces a patch method under
the node module to be able to evaluate the reset_interfaces
parameter.
Also, it modifies the _prepare_request method in resource
to accept query parameters in urls and build the request
uri keeping those into account.

Increasing minimum version of mock to 3.0.0 to be able to use
testing features not supported before that version, e.g.
assert_called_once

Change-Id: I8bca403df7d38a7ac1d066c5f1d7e2bff1deb054
This commit is contained in:
Riccardo Pittau 2019-10-18 12:24:33 +02:00
parent 25d9aa7de8
commit 13c6bc2bd9
7 changed files with 106 additions and 5 deletions

View File

@ -17,7 +17,7 @@ jsonpointer==1.13
jsonschema==2.6.0
keystoneauth1==3.18.0
linecache2==1.0.0
mock==2.0.0
mock==3.0.0
mox3==0.20.0
munch==2.1.0
netifaces==0.10.4

View File

@ -294,12 +294,16 @@ class Proxy(proxy.Proxy):
res = self._get_resource(_node.Node, node, **attrs)
return res.commit(self, retry_on_conflict=retry_on_conflict)
def patch_node(self, node, patch, retry_on_conflict=True):
def patch_node(self, node, patch, reset_interfaces=None,
retry_on_conflict=True):
"""Apply a JSON patch to 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 patch: JSON patch to apply.
:param bool reset_interfaces: whether to reset the node hardware
interfaces to their defaults. This works only when changing
drivers. Added in API microversion 1.45.
:param bool retry_on_conflict: Whether to retry HTTP CONFLICT error.
Most of the time it can be retried, since it is caused by the node
being locked. However, when setting ``instance_id``, this is
@ -313,7 +317,8 @@ class Proxy(proxy.Proxy):
:rtype: :class:`~openstack.baremetal.v1.node.Node`
"""
res = self._get_resource(_node.Node, node)
return res.patch(self, patch, retry_on_conflict=retry_on_conflict)
return res.patch(self, patch, retry_on_conflict=retry_on_conflict,
reset_interfaces=reset_interfaces)
def set_node_provision_state(self, node, target, config_drive=None,
clean_steps=None, rescue_password=None,

View File

@ -833,5 +833,39 @@ class Node(_common.ListMixin, resource.Resource):
self.traits = traits
def patch(self, session, patch=None, prepend_key=True, has_body=True,
retry_on_conflict=None, base_path=None, reset_interfaces=None):
if reset_interfaces is not None:
# The id cannot be dirty for an commit
self._body._dirty.discard("id")
# Only try to update if we actually have anything to commit.
if not patch and not self.requires_commit:
return self
if not self.allow_patch:
raise exceptions.MethodNotSupported(self, "patch")
session = self._get_session(session)
microversion = utils.pick_microversion(session, '1.45')
params = [('reset_interfaces', reset_interfaces)]
request = self._prepare_request(requires_id=True,
prepend_key=prepend_key,
base_path=base_path, patch=True,
params=params)
if patch:
request.body += self._convert_patch(patch)
return self._commit(session, request, 'PATCH', microversion,
has_body=has_body,
retry_on_conflict=retry_on_conflict)
else:
return super(Node, self).patch(session, patch=patch,
retry_on_conflict=retry_on_conflict)
NodeDetail = Node

View File

@ -1053,7 +1053,7 @@ class Resource(dict):
return body
def _prepare_request(self, requires_id=None, prepend_key=False,
patch=False, base_path=None):
patch=False, base_path=None, params=None):
"""Prepare a request to be sent to the server
Create operations don't require an ID, but all others do,
@ -1091,6 +1091,10 @@ class Resource(dict):
uri = utils.urljoin(uri, self.id)
if params:
query_params = six.moves.urllib.parse.urlencode(params)
uri += '?' + query_params
return _Request(uri, body, headers)
def _translate_response(self, response, has_body=None, error_message=None):

View File

@ -16,6 +16,7 @@ import mock
from openstack.baremetal.v1 import _common
from openstack.baremetal.v1 import node
from openstack import exceptions
from openstack import resource
from openstack.tests.unit import base
# NOTE: Sample data from api-ref doc
@ -766,3 +767,39 @@ class TestNodeTraits(base.TestCase):
json={'traits': ['CUSTOM_FAKE', 'CUSTOM_REAL', 'CUSTOM_MISSING']},
headers=mock.ANY, microversion='1.37',
retriable_status_codes=_common.RETRIABLE_STATUS_CODES)
@mock.patch.object(resource.Resource, 'patch', autospec=True)
class TestNodePatch(base.TestCase):
def setUp(self):
super(TestNodePatch, self).setUp()
self.node = node.Node(**FAKE)
self.session = mock.Mock(spec=adapter.Adapter,
default_microversion=None)
self.session.log = mock.Mock()
def test_node_patch(self, mock_patch):
patch = {'path': 'test'}
self.node.patch(self.session, patch=patch)
mock_patch.assert_called_once()
kwargs = mock_patch.call_args.kwargs
self.assertEqual(kwargs['patch'], {'path': 'test'})
@mock.patch.object(resource.Resource, '_prepare_request', autospec=True)
@mock.patch.object(resource.Resource, '_commit', autospec=True)
def test_node_patch_reset_interfaces(self, mock__commit, mock_prepreq,
mock_patch):
patch = {'path': 'test'}
self.node.patch(self.session, patch=patch, retry_on_conflict=True,
reset_interfaces=True)
mock_prepreq.assert_called_once()
prepreq_kwargs = mock_prepreq.call_args.kwargs
self.assertEqual(prepreq_kwargs['params'],
[('reset_interfaces', True)])
mock__commit.assert_called_once()
commit_args = mock__commit.call_args.args
commit_kwargs = mock__commit.call_args.kwargs
self.assertIn('1.45', commit_args)
self.assertEqual(commit_kwargs['retry_on_conflict'], True)
mock_patch.assert_not_called()

View File

@ -1104,6 +1104,27 @@ class TestResource(base.TestCase):
self.assertEqual([{'op': 'add', 'path': '/x', 'value': 1}],
result.body)
def test__prepare_request_with_patch_params(self):
class Test(resource.Resource):
commit_jsonpatch = True
base_path = "/something"
x = resource.Body("x")
y = resource.Body("y")
the_id = "id"
sot = Test.existing(id=the_id, x=1, y=2)
sot.x = 3
params = [('foo', 'bar'),
('life', 42)]
result = sot._prepare_request(requires_id=True, patch=True,
params=params)
self.assertEqual("something/id?foo=bar&life=42", result.url)
self.assertEqual([{'op': 'replace', 'path': '/x', 'value': 3}],
result.body)
def test__translate_response_no_body(self):
class Test(resource.Resource):
attr = resource.Header("attr")

View File

@ -8,7 +8,7 @@ ddt>=1.0.1 # MIT
extras>=1.0.0 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD
jsonschema>=2.6.0 # MIT
mock>=2.0.0 # BSD
mock>=3.0.0 # BSD
prometheus-client>=0.4.2 # Apache-2.0
python-subunit>=1.0.0 # Apache-2.0/BSD
oslo.config>=6.1.0 # Apache-2.0