Use direct calls to get_<resource>_by_id

This commit adds a `use_direct_get` flag to the OpenStackCloud
object. The goal is to enable direct calls to the
get_<resource>_by_id methods when a UUID is passed and default
to the `list` and `search` calls otherwise.

Change-Id: I6aebfe7cb40adace0568d8f131e64d6555736712
Signed-off-by: Rosario Di Somma <rosario.disomma@dreamhost.com>
This commit is contained in:
Rosario Di Somma 2017-08-17 12:58:03 +00:00 committed by Monty Taylor
parent ffa5425df6
commit 149a9fbc78
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
5 changed files with 145 additions and 44 deletions

View File

@ -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_<resource>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_<resorce_name>_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

View File

@ -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)

View File

@ -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
@ -835,7 +837,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')
@ -964,8 +966,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.
@ -2599,7 +2600,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.
@ -2624,7 +2625,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
@ -2663,7 +2664,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.
@ -2684,7 +2685,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
@ -2722,7 +2723,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
@ -2762,7 +2763,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.
@ -2787,7 +2788,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
@ -2828,7 +2829,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.
@ -2858,7 +2859,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
@ -2920,7 +2921,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
@ -3009,7 +3010,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):
@ -3045,7 +3046,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):
@ -3071,7 +3072,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
@ -3162,7 +3163,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
@ -3215,7 +3216,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.
@ -5177,7 +5178,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,
@ -5236,7 +5237,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):
@ -6482,7 +6483,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]
@ -8163,7 +8163,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()
@ -8455,7 +8455,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

View File

@ -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: <endpoint internal url> (optional)
- admin_url: <endpoint 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.

View File

@ -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_<resource> 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_<resource> 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))