From 6b62d2815131f327f093e4b95489a31efa7d9d3c Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Fri, 4 Nov 2022 17:23:00 +0100 Subject: [PATCH] Implement unified search_resources method We have a lot of search_XXX calls in the cloud layer, but not for everything. They are also doing nearly same to what it is possible to do with plain proxy methods and therfore we want to simplify and unify this so that Ansible modules can easily rely on a single function in different modules instead or needing a dedicated search method for every resource doing the same. New method accepts resource_type, resource identifier (which may be empty), filters and also possibility to pass additional args into the _get and _list calls (for unpredicted special cases). With this all search_XXX functions can be finally deprecated. Change-Id: I375c2b625698c4920211eb6e089a1b820755be84 --- openstack/cloud/openstackcloud.py | 71 +++++++++ .../tests/unit/cloud/test_openstackcloud.py | 135 ++++++++++++++++++ .../search_resource-b9c2f772e01d3b2c.yaml | 7 + 3 files changed, 213 insertions(+) create mode 100644 openstack/tests/unit/cloud/test_openstackcloud.py create mode 100644 releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml diff --git a/openstack/cloud/openstackcloud.py b/openstack/cloud/openstackcloud.py index 0b5cbb527..9e715cb9d 100644 --- a/openstack/cloud/openstackcloud.py +++ b/openstack/cloud/openstackcloud.py @@ -753,6 +753,77 @@ class _OpenStackCloudMixin: else: return False + def search_resources( + self, + resource_type, + name_or_id, + get_args=None, + get_kwargs=None, + list_args=None, + list_kwargs=None, + **filters + ): + """Search resources + + Search resources matching certain conditions + + :param str resource_type: String representation of the expected + resource as `service.resource` (i.e. "network.security_group"). + :param str name_or_id: Name or ID of the resource + :param list get_args: Optional args to be passed to the _get call. + :param dict get_kwargs: Optional kwargs to be passed to the _get call. + :param list list_args: Optional args to be passed to the _list call. + :param dict list_kwargs: Optional kwargs to be passed to the _list call + :param dict filters: Additional filters to be used for querying + resources. + """ + get_args = get_args or () + get_kwargs = get_kwargs or {} + list_args = list_args or () + list_kwargs = list_kwargs or {} + + # User used string notation. Try to find proper + # resource + (service_name, resource_name) = resource_type.split('.') + if not hasattr(self, service_name): + raise exceptions.SDKException( + "service %s is not existing/enabled" % + service_name + ) + service_proxy = getattr(self, service_name) + try: + resource_type = service_proxy._resource_registry[resource_name] + except KeyError: + raise exceptions.SDKException( + "Resource %s is not known in service %s" % + (resource_name, service_name) + ) + + if name_or_id: + # name_or_id is definitely not None + try: + resource_by_id = service_proxy._get( + resource_type, + name_or_id, + *get_args, + **get_kwargs) + return [resource_by_id] + except exceptions.ResourceNotFound: + pass + + if not filters: + filters = {} + + if name_or_id: + filters["name"] = name_or_id + list_kwargs.update(filters) + + return list(service_proxy._list( + resource_type, + *list_args, + **list_kwargs + )) + def project_cleanup( self, dry_run=True, diff --git a/openstack/tests/unit/cloud/test_openstackcloud.py b/openstack/tests/unit/cloud/test_openstackcloud.py new file mode 100644 index 000000000..07d227995 --- /dev/null +++ b/openstack/tests/unit/cloud/test_openstackcloud.py @@ -0,0 +1,135 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from openstack import exceptions +from openstack import proxy +from openstack import resource +from openstack.tests.unit import base + + +class TestSearch(base.TestCase): + + class FakeResource(resource.Resource): + allow_fetch = True + allow_list = True + + foo = resource.Body("foo") + + def setUp(self): + super(TestSearch, self).setUp() + + self.session = proxy.Proxy(self.cloud) + self.session._sdk_connection = self.cloud + self.session._get = mock.Mock() + self.session._list = mock.Mock() + self.session._resource_registry = dict( + fake=self.FakeResource + ) + # Set the mock into the cloud connection + setattr(self.cloud, "mock_session", self.session) + + def test_raises_unknown_service(self): + self.assertRaises( + exceptions.SDKException, + self.cloud.search_resources, + "wrong_service.wrong_resource", + "name" + ) + + def test_raises_unknown_resource(self): + self.assertRaises( + exceptions.SDKException, + self.cloud.search_resources, + "mock_session.wrong_resource", + "name" + ) + + def test_search_resources_get_finds(self): + self.session._get.return_value = self.FakeResource(foo="bar") + + ret = self.cloud.search_resources( + "mock_session.fake", + "fake_name" + ) + self.session._get.assert_called_with( + self.FakeResource, "fake_name") + + self.assertEqual(1, len(ret)) + self.assertEqual( + self.FakeResource(foo="bar").to_dict(), + ret[0].to_dict() + ) + + def test_search_resources_list(self): + self.session._get.side_effect = exceptions.ResourceNotFound + self.session._list.return_value = [ + self.FakeResource(foo="bar") + ] + + ret = self.cloud.search_resources( + "mock_session.fake", + "fake_name" + ) + self.session._get.assert_called_with( + self.FakeResource, "fake_name") + self.session._list.assert_called_with( + self.FakeResource, name="fake_name") + + self.assertEqual(1, len(ret)) + self.assertEqual( + self.FakeResource(foo="bar").to_dict(), + ret[0].to_dict() + ) + + def test_search_resources_args(self): + self.session._get.side_effect = exceptions.ResourceNotFound + self.session._list.return_value = [] + + self.cloud.search_resources( + "mock_session.fake", + "fake_name", + get_args=["getarg1"], + get_kwargs={"getkwarg1": "1"}, + list_args=["listarg1"], + list_kwargs={"listkwarg1": "1"}, + filter1="foo" + ) + self.session._get.assert_called_with( + self.FakeResource, "fake_name", + "getarg1", getkwarg1="1") + self.session._list.assert_called_with( + self.FakeResource, + "listarg1", listkwarg1="1", + name="fake_name", filter1="foo" + ) + + def test_search_resources_name_empty(self): + self.session._list.return_value = [ + self.FakeResource(foo="bar") + ] + + ret = self.cloud.search_resources( + "mock_session.fake", + None, + foo="bar" + ) + self.session._get.assert_not_called() + self.session._list.assert_called_with( + self.FakeResource, foo="bar") + + self.assertEqual(1, len(ret)) + self.assertEqual( + self.FakeResource(foo="bar").to_dict(), + ret[0].to_dict() + ) diff --git a/releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml b/releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml new file mode 100644 index 000000000..70efca4a2 --- /dev/null +++ b/releasenotes/notes/search_resource-b9c2f772e01d3b2c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add search_resources method implementing generic search interface accepting + resource name (as "service.resource"), name_or_id and list of additional + filters and returning 0 or many resources matching those. This interface is + primarily designed to be used by Ansible modules.