diff --git a/shade/_utils.py b/shade/_utils.py index bd57a3fdf..37939e5b2 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -23,6 +23,7 @@ import six import sre_constants import sys import time +import uuid from decorator import decorator @@ -196,12 +197,15 @@ def _filter_list(data, name_or_id, filters): return filtered -def _get_entity(func, name_or_id, filters, **kwargs): +def _get_entity(cloud, resource, name_or_id, filters, **kwargs): """Return a single entity from the list returned by a given method. - :param callable func: - A function that takes `name_or_id` and `filters` as parameters - and returns a list of entities to filter. + :param object cloud: + The controller class (Example: the main OpenStackCloud object) . + :param string or callable resource: + The string that identifies the resource to use to lookup the + get_<>_by_id or search_s methods(Example: network) + or a callable to invoke. :param string name_or_id: The name or ID of the entity being filtered or a dict :param filters: @@ -210,20 +214,33 @@ def _get_entity(func, name_or_id, filters, **kwargs): A string containing a jmespath expression for further filtering. Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]" """ + # Sometimes in the control flow of shade, we already have an object # fetched. Rather than then needing to pull the name or id out of that # object, pass it in here and rely on caching to prevent us from making # an additional call, it's simple enough to test to see if we got an # object and just short-circuit return it. + if hasattr(name_or_id, 'id'): return name_or_id - entities = func(name_or_id, filters, **kwargs) - if not entities: - return None - if len(entities) > 1: - raise exc.OpenStackCloudException( - "Multiple matches found for %s" % name_or_id) - return entities[0] + + # If a uuid is passed short-circuit it calling the + # get__by_id method + if getattr(cloud, 'use_direct_get', False) and _is_uuid_like(name_or_id): + get_resource = getattr(cloud, 'get_%s_by_id' % resource, None) + if get_resource: + return get_resource(name_or_id) + + search = resource if callable(resource) else getattr( + cloud, 'search_%ss' % resource, None) + if search: + entities = search(name_or_id, filters, **kwargs) + if entities: + if len(entities) > 1: + raise exc.OpenStackCloudException( + "Multiple matches found for %s" % name_or_id) + return entities[0] + return None def normalize_keystone_services(services): @@ -670,3 +687,27 @@ class FileSegment(object): def reset(self): self._file.seek(self.offset, 0) + + +def _format_uuid_string(string): + return (string.replace('urn:', '') + .replace('uuid:', '') + .strip('{}') + .replace('-', '') + .lower()) + + +def _is_uuid_like(val): + """Returns validation of a value as a UUID. + + :param val: Value to verify + :type val: string + :returns: bool + + .. versionchanged:: 1.1.1 + Support non-lowercase UUIDs. + """ + try: + return str(uuid.UUID(val)).replace('-', '') == _format_uuid_string(val) + except (TypeError, ValueError, AttributeError): + return False diff --git a/shade/inventory.py b/shade/inventory.py index 101682a18..2490e93bf 100644 --- a/shade/inventory.py +++ b/shade/inventory.py @@ -27,7 +27,8 @@ class OpenStackInventory(object): def __init__( self, config_files=None, refresh=False, private=False, - config_key=None, config_defaults=None, cloud=None): + config_key=None, config_defaults=None, cloud=None, + use_direct_get=False): if config_files is None: config_files = [] config = os_client_config.config.OpenStackConfig( @@ -82,4 +83,4 @@ class OpenStackInventory(object): func = self.search_hosts else: func = functools.partial(self.search_hosts, expand=False) - return _utils._get_entity(func, name_or_id, filters) + return _utils._get_entity(self, func, name_or_id, filters) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 2b6c7d770..270e47a63 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1,4 +1,4 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); +# Licensed under the Apache License, Version 3.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # @@ -138,6 +138,7 @@ class OpenStackCloud( strict=False, app_name=None, app_version=None, + use_direct_get=False, **kwargs): if log_inner_exceptions: @@ -211,6 +212,7 @@ class OpenStackCloud( warnings.filterwarnings('ignore', category=category) self._disable_warnings = {} + self.use_direct_get = use_direct_get self._servers = None self._servers_time = 0 @@ -838,7 +840,7 @@ class OpenStackCloud( :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self.search_projects, name_or_id, filters, + return _utils._get_entity(self, 'project', name_or_id, filters, domain_id=domain_id) @_utils.valid_kwargs('description') @@ -967,8 +969,7 @@ class OpenStackCloud( :raises: ``OpenStackCloudException``: if something goes wrong during the OpenStack API call. """ - return _utils._get_entity(self.search_users, name_or_id, filters, - **kwargs) + return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs) def get_user_by_id(self, user_id, normalize=True): """Get a user by ID. @@ -2597,7 +2598,7 @@ class OpenStackCloud( :returns: A keypair ``munch.Munch`` or None if no matching keypair is found. """ - return _utils._get_entity(self.search_keypairs, name_or_id, filters) + return _utils._get_entity(self, 'keypair', name_or_id, filters) def get_network(self, name_or_id, filters=None): """Get a network by name or ID. @@ -2622,7 +2623,7 @@ class OpenStackCloud( found. """ - return _utils._get_entity(self.search_networks, name_or_id, filters) + return _utils._get_entity(self, 'network', name_or_id, filters) def get_network_by_id(self, id): """ Get a network by ID @@ -2661,7 +2662,7 @@ class OpenStackCloud( found. """ - return _utils._get_entity(self.search_routers, name_or_id, filters) + return _utils._get_entity(self, 'router', name_or_id, filters) def get_subnet(self, name_or_id, filters=None): """Get a subnet by name or ID. @@ -2682,7 +2683,7 @@ class OpenStackCloud( found. """ - return _utils._get_entity(self.search_subnets, name_or_id, filters) + return _utils._get_entity(self, 'subnet', name_or_id, filters) def get_subnet_by_id(self, id): """ Get a subnet by ID @@ -2720,7 +2721,7 @@ class OpenStackCloud( :returns: A port ``munch.Munch`` or None if no matching port is found. """ - return _utils._get_entity(self.search_ports, name_or_id, filters) + return _utils._get_entity(self, 'port', name_or_id, filters) def get_port_by_id(self, id): """ Get a port by ID @@ -2760,7 +2761,7 @@ class OpenStackCloud( """ return _utils._get_entity( - self.search_qos_policies, name_or_id, filters) + self, 'qos_policie', name_or_id, filters) def get_volume(self, name_or_id, filters=None): """Get a volume by name or ID. @@ -2785,7 +2786,7 @@ class OpenStackCloud( found. """ - return _utils._get_entity(self.search_volumes, name_or_id, filters) + return _utils._get_entity(self, 'volume', name_or_id, filters) def get_volume_by_id(self, id): """ Get a volume by ID @@ -2826,7 +2827,7 @@ class OpenStackCloud( """ return _utils._get_entity( - self.search_volume_types, name_or_id, filters) + self, 'volume_type', name_or_id, filters) def get_flavor(self, name_or_id, filters=None, get_extra=True): """Get a flavor by name or ID. @@ -2856,7 +2857,7 @@ class OpenStackCloud( """ search_func = functools.partial( self.search_flavors, get_extra=get_extra) - return _utils._get_entity(search_func, name_or_id, filters) + return _utils._get_entity(self, search_func, name_or_id, filters) def get_flavor_by_id(self, id, get_extra=True): """ Get a flavor by ID @@ -2918,7 +2919,7 @@ class OpenStackCloud( """ return _utils._get_entity( - self.search_security_groups, name_or_id, filters) + self, 'security_group', name_or_id, filters) def get_security_group_by_id(self, id): """ Get a security group by ID @@ -3007,7 +3008,7 @@ class OpenStackCloud( """ searchfunc = functools.partial(self.search_servers, detailed=detailed, bare=True) - server = _utils._get_entity(searchfunc, name_or_id, filters) + server = _utils._get_entity(self, searchfunc, name_or_id, filters) return self._expand_server(server, detailed, bare) def _expand_server(self, server, detailed, bare): @@ -3043,7 +3044,7 @@ class OpenStackCloud( is found. """ - return _utils._get_entity(self.search_server_groups, name_or_id, + return _utils._get_entity(self, 'server_group', name_or_id, filters) def get_image(self, name_or_id, filters=None): @@ -3069,7 +3070,7 @@ class OpenStackCloud( is found """ - return _utils._get_entity(self.search_images, name_or_id, filters) + return _utils._get_entity(self, 'image', name_or_id, filters) def get_image_by_id(self, id): """ Get a image by ID @@ -3160,7 +3161,7 @@ class OpenStackCloud( IP is found. """ - return _utils._get_entity(self.search_floating_ips, id, filters) + return _utils._get_entity(self, 'floating_ip', id, filters) def get_floating_ip_by_id(self, id): """ Get a floating ip by ID @@ -3213,7 +3214,7 @@ class OpenStackCloud( return _utils._filter_list([stack], name_or_id, filters) return _utils._get_entity( - _search_one_stack, name_or_id, filters) + self, _search_one_stack, name_or_id, filters) def create_keypair(self, name, public_key=None): """Create a new keypair. @@ -5175,7 +5176,7 @@ class OpenStackCloud( :returns: A volume ``munch.Munch`` or None if no matching volume is found. """ - return _utils._get_entity(self.search_volume_snapshots, name_or_id, + return _utils._get_entity(self, 'volume_snapshot', name_or_id, filters) def create_volume_backup(self, volume_id, name=None, description=None, @@ -5234,7 +5235,7 @@ class OpenStackCloud( :returns: A backup ``munch.Munch`` or None if no matching backup is found. """ - return _utils._get_entity(self.search_volume_backups, name_or_id, + return _utils._get_entity(self, 'volume_backup', name_or_id, filters) def list_volume_snapshots(self, detailed=True, search_opts=None): @@ -6480,7 +6481,6 @@ class OpenStackCloud( :raises: OpenStackCloudException on operation error. """ # TODO(mordred) Add support for description starting in 2.19 - security_groups = kwargs.get('security_groups', []) if security_groups and not isinstance(kwargs['security_groups'], list): security_groups = [security_groups] @@ -8161,7 +8161,7 @@ class OpenStackCloud( :returns: A zone dict or None if no matching zone is found. """ - return _utils._get_entity(self.search_zones, name_or_id, filters) + return _utils._get_entity(self, 'zone', name_or_id, filters) def search_zones(self, name_or_id=None, filters=None): zones = self.list_zones() @@ -8453,7 +8453,7 @@ class OpenStackCloud( :returns: A cluster template dict or None if no matching cluster template is found. """ - return _utils._get_entity(self.search_cluster_templates, name_or_id, + return _utils._get_entity(self, 'cluster_template', name_or_id, filters=filters, detail=detail) get_baymodel = get_cluster_template diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 1d74ee32d..6fac151d4 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -849,7 +849,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud): :raises: ``OpenStackCloudException`` if something goes wrong during the openstack API call or if multiple matches are found. """ - return _utils._get_entity(self.search_services, name_or_id, filters) + return _utils._get_entity(self, 'service', name_or_id, filters) def delete_service(self, name_or_id): """Delete a Keystone service. @@ -1057,7 +1057,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud): - internal_url: (optional) - admin_url: (optional) """ - return _utils._get_entity(self.search_endpoints, id, filters) + return _utils._get_entity(self, 'endpoint', id, filters) def delete_endpoint(self, id): """Delete a Keystone endpoint. @@ -1226,7 +1226,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud): # duplicate that logic here if hasattr(name_or_id, 'id'): return name_or_id - return _utils._get_entity(self.search_domains, filters, name_or_id) + return _utils._get_entity(self, 'domain', filters, name_or_id) else: error_msg = 'Failed to get domain {id}'.format(id=domain_id) data = self._identity_client.get( @@ -1281,8 +1281,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - return _utils._get_entity(self.search_groups, name_or_id, filters, - **kwargs) + return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs) def create_group(self, name, description, domain=None): """Create a group. @@ -1424,7 +1423,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud): :raises: ``OpenStackCloudException``: if something goes wrong during the openstack API call. """ - return _utils._get_entity(self.search_roles, name_or_id, filters) + return _utils._get_entity(self, 'role', name_or_id, filters) def _keystone_v2_role_assignments(self, user, project=None, role=None, **kwargs): @@ -1900,7 +1899,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud): found. """ - return _utils._get_entity(self.search_aggregates, name_or_id, filters) + return _utils._get_entity(self, 'aggregate', name_or_id, filters) def create_aggregate(self, name, availability_zone=None): """Create a new host aggregate. diff --git a/shade/tests/unit/test__utils.py b/shade/tests/unit/test__utils.py index 24da072f2..1c453f426 100644 --- a/shade/tests/unit/test__utils.py +++ b/shade/tests/unit/test__utils.py @@ -15,7 +15,9 @@ import random import string import tempfile +from uuid import uuid4 +import mock import testtools from shade import _utils @@ -318,3 +320,61 @@ class TestUtils(base.TestCase): name) segment_content += segment.read() self.assertEqual(content, segment_content) + + def test_get_entity_pass_object(self): + obj = mock.Mock(id=uuid4().hex) + self.cloud.use_direct_get = True + self.assertEqual(obj, _utils._get_entity(self.cloud, '', obj, {})) + + def test_get_entity_no_use_direct_get(self): + # test we are defaulting to the search_ methods + # if the use_direct_get flag is set to False(default). + uuid = uuid4().hex + resource = 'network' + func = 'search_%ss' % resource + filters = {} + with mock.patch.object(self.cloud, func) as search: + _utils._get_entity(self.cloud, resource, uuid, filters) + search.assert_called_once_with(uuid, filters) + + def test_get_entity_no_uuid_like(self): + # test we are defaulting to the search_ methods + # if the name_or_id param is a name(string) but not a uuid. + self.cloud.use_direct_get = True + name = 'name_no_uuid' + resource = 'network' + func = 'search_%ss' % resource + filters = {} + with mock.patch.object(self.cloud, func) as search: + _utils._get_entity(self.cloud, resource, name, filters) + search.assert_called_once_with(name, filters) + + def test_get_entity_pass_uuid(self): + uuid = uuid4().hex + self.cloud.use_direct_get = True + resources = ['flavor', 'image', 'volume', 'network', + 'subnet', 'port', 'floating_ip', 'security_group'] + for r in resources: + f = 'get_%s_by_id' % r + with mock.patch.object(self.cloud, f) as get: + _utils._get_entity(self.cloud, r, uuid, {}) + get.assert_called_once_with(uuid) + + def test_get_entity_pass_search_methods(self): + self.cloud.use_direct_get = True + resources = ['flavor', 'image', 'volume', 'network', + 'subnet', 'port', 'floating_ip', 'security_group'] + filters = {} + name = 'name_no_uuid' + for r in resources: + f = 'search_%ss' % r + with mock.patch.object(self.cloud, f) as search: + _utils._get_entity(self.cloud, r, name, {}) + search.assert_called_once_with(name, filters) + + def test_get_entity_get_and_search(self): + resources = ['flavor', 'image', 'volume', 'network', + 'subnet', 'port', 'floating_ip', 'security_group'] + for r in resources: + self.assertTrue(hasattr(self.cloud, 'get_%s_by_id' % r)) + self.assertTrue(hasattr(self.cloud, 'search_%ss' % r))