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.