Add method for bulk creating objects.

There are APIs (like Neutron) which provide a way for creating multiple
objects at once using single request. This change add possibility for
performing such requests using Resource objects.

Change-Id: I7d7c540ed1bada37ebebbe305dc0e36f38cff071
This commit is contained in:
gryf
2020-02-17 11:46:11 +01:00
parent 36cda6086c
commit ce3646fa27
2 changed files with 207 additions and 0 deletions

View File

@@ -1286,6 +1286,83 @@ class Resource(dict):
return self.fetch(session)
return self
@classmethod
def bulk_create(cls, session, data, prepend_key=True, base_path=None,
**params):
"""Create multiple remote resources based on this class and data.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param data: list of dicts, which represent resources to create.
:param prepend_key: A boolean indicating whether the resource_key
should be prepended in a resource creation
request. Default to True.
:param str base_path: Base part of the URI for creating resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:param dict params: Additional params to pass.
:return: A generator of :class:`Resource` objects.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
:data:`Resource.allow_create` is not set to ``True``.
"""
if not cls.allow_create:
raise exceptions.MethodNotSupported(cls, "create")
if not (data and isinstance(data, list)
and all([isinstance(x, dict) for x in data])):
raise ValueError('Invalid data passed: %s' % data)
session = cls._get_session(session)
microversion = cls._get_microversion_for(cls, session, 'create')
requires_id = (cls.create_requires_id
if cls.create_requires_id is not None
else cls.create_method == 'PUT')
if cls.create_method == 'PUT':
method = session.put
elif cls.create_method == 'POST':
method = session.post
else:
raise exceptions.ResourceFailure(
msg="Invalid create method: %s" % cls.create_method)
body = []
resources = []
for attrs in data:
# NOTE(gryf): we need to create resource objects, since
# _prepare_request only works on instances, not classes.
# Those objects will be used in case where request doesn't return
# JSON data representing created resource, and yet it's required
# to return newly created resource objects.
resource = cls.new(connection=session._get_connection(), **attrs)
resources.append(resource)
request = resource._prepare_request(requires_id=requires_id,
base_path=base_path)
body.append(request.body)
if prepend_key:
body = {cls.resources_key: body}
response = method(request.url, json=body, headers=request.headers,
microversion=microversion, params=params)
exceptions.raise_from_response(response)
data = response.json()
if cls.resources_key:
data = data[cls.resources_key]
if not isinstance(data, list):
data = [data]
has_body = (cls.has_body if cls.create_returns_body is None
else cls.create_returns_body)
if has_body and cls.create_returns_body is False:
return (r.fetch(session) for r in resources)
else:
return (cls.existing(microversion=microversion,
connection=session._get_connection(),
**res_dict) for res_dict in data)
def fetch(self, session, requires_id=True,
base_path=None, error_message=None, **params):
"""Get a remote resource based on this instance.

View File

@@ -2487,6 +2487,136 @@ class TestResourceActions(base.TestCase):
# Ensure we only made two calls to get this done
self.assertEqual(2, len(self.session.get.call_args_list))
def test_bulk_create_invalid_data_passed(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'POST'
allow_create = True
Test._prepare_request = mock.Mock()
self.assertRaises(ValueError, Test.bulk_create, self.session, [])
self.assertRaises(ValueError, Test.bulk_create, self.session, None)
self.assertRaises(ValueError, Test.bulk_create, self.session, object)
self.assertRaises(ValueError, Test.bulk_create, self.session, {})
self.assertRaises(ValueError, Test.bulk_create, self.session, "hi!")
self.assertRaises(ValueError, Test.bulk_create, self.session, ["hi!"])
def _test_bulk_create(self, cls, http_method, microversion=None,
base_path=None, **params):
req1 = mock.Mock()
req2 = mock.Mock()
req1.body = {'name': 'resource1'}
req2.body = {'name': 'resource2'}
req1.url = 'uri'
req2.url = 'uri'
req1.headers = 'headers'
req2.headers = 'headers'
request_body = {"tests": [{'name': 'resource1', 'id': 'id1'},
{'name': 'resource2', 'id': 'id2'}]}
cls._prepare_request = mock.Mock(side_effect=[req1, req2])
mock_response = mock.Mock()
mock_response.status_code = 200
mock_response.links = {}
mock_response.json.return_value = request_body
http_method.return_value = mock_response
res = list(cls.bulk_create(self.session, [{'name': 'resource1'},
{'name': 'resource2'}],
base_path=base_path, **params))
self.assertEqual(len(res), 2)
self.assertEqual(res[0].id, 'id1')
self.assertEqual(res[1].id, 'id2')
http_method.assert_called_once_with(self.request.url,
json={'tests': [req1.body,
req2.body]},
headers=self.request.headers,
microversion=microversion,
params=params)
def test_bulk_create_post(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'POST'
allow_create = True
resources_key = 'tests'
self._test_bulk_create(Test, self.session.post)
def test_bulk_create_put(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'PUT'
allow_create = True
resources_key = 'tests'
self._test_bulk_create(Test, self.session.put)
def test_bulk_create_with_params(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'POST'
allow_create = True
resources_key = 'tests'
self._test_bulk_create(Test, self.session.post, answer=42)
def test_bulk_create_with_microversion(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'POST'
allow_create = True
resources_key = 'tests'
_max_microversion = '1.42'
self._test_bulk_create(Test, self.session.post, microversion='1.42')
def test_bulk_create_with_base_path(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'POST'
allow_create = True
resources_key = 'tests'
self._test_bulk_create(Test, self.session.post, base_path='dummy')
def test_bulk_create_fail(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'POST'
allow_create = False
resources_key = 'tests'
self.assertRaises(exceptions.MethodNotSupported, Test.bulk_create,
self.session, [{'name': 'name'}])
def test_bulk_create_fail_on_request(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
create_method = 'POST'
allow_create = True
resources_key = 'tests'
response = FakeResponse({}, status_code=409)
response.content = ('{"TestError": {"message": "Failed to parse '
'request. Required attribute \'foo\' not '
'specified", "type": "HTTPBadRequest", '
'"detail": ""}}')
response.reason = 'Bad Request'
self.session.post.return_value = response
self.assertRaises(exceptions.ConflictException, Test.bulk_create,
self.session, [{'name': 'name'}])
class TestResourceFind(base.TestCase):