diff --git a/lower-constraints.txt b/lower-constraints.txt index 7cdec6680..c4e6ec4d7 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -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 diff --git a/openstack/baremetal/v1/_proxy.py b/openstack/baremetal/v1/_proxy.py index eea434227..0e00b3914 100644 --- a/openstack/baremetal/v1/_proxy.py +++ b/openstack/baremetal/v1/_proxy.py @@ -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, diff --git a/openstack/baremetal/v1/node.py b/openstack/baremetal/v1/node.py index 2f4ff1fe1..6bb099c3a 100644 --- a/openstack/baremetal/v1/node.py +++ b/openstack/baremetal/v1/node.py @@ -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 diff --git a/openstack/resource.py b/openstack/resource.py index 59f38a4d2..fe266c6f5 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -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): diff --git a/openstack/tests/unit/baremetal/v1/test_node.py b/openstack/tests/unit/baremetal/v1/test_node.py index 2669eaf30..a1a4dc04f 100644 --- a/openstack/tests/unit/baremetal/v1/test_node.py +++ b/openstack/tests/unit/baremetal/v1/test_node.py @@ -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() diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index 43e47e91b..ff29ba83d 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -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") diff --git a/test-requirements.txt b/test-requirements.txt index ce8b81d83..bbd290487 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -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