diff --git a/openstack/resource.py b/openstack/resource.py index 1a88d8fd5..9260e9577 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -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. diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index ff29ba83d..1e47d89af 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -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):