Add support for jmespath filter expressions

ansible upstream recently added a filter using jmespath to allow for
arbitrary querying of complex data structures. While reviewing that
patch, it seemed quite flexible and lovely. It's also apparently also
used in boto for similar reasons, so we're following a pretty well
tested solution - and since it's now a depend on ansible, the extra
depend shouldn't be too much of a burden for most folks.

Change-Id: Ia4bf455f0e32f29a6fce79c71fecce7b0ed57ea5
This commit is contained in:
Monty Taylor 2016-09-07 13:51:15 -05:00
parent 1c00116195
commit e888d8e5cd
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
6 changed files with 93 additions and 20 deletions

View File

@ -0,0 +1,4 @@
---
features:
- All get and search functions can now take a jmespath expression in their
filters parameter.

View File

@ -2,6 +2,7 @@ pbr>=0.11,<2.0
munch munch
decorator decorator
jmespath
jsonpatch jsonpatch
ipaddress ipaddress
os-client-config>=1.20.0 os-client-config>=1.20.0

View File

@ -14,6 +14,7 @@
import contextlib import contextlib
import inspect import inspect
import jmespath
import munch import munch
import netifaces import netifaces
import re import re
@ -73,7 +74,7 @@ def _filter_list(data, name_or_id, filters):
key if a value for name_or_id is given. key if a value for name_or_id is given.
:param string name_or_id: :param string name_or_id:
The name or ID of the entity being filtered. The name or ID of the entity being filtered.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -83,6 +84,8 @@ def _filter_list(data, name_or_id, filters):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
""" """
if name_or_id: if name_or_id:
identifier_matches = [] identifier_matches = []
@ -96,6 +99,9 @@ def _filter_list(data, name_or_id, filters):
if not filters: if not filters:
return data return data
if isinstance(filters, six.string_types):
return jmespath.search(filters, data)
def _dict_filter(f, d): def _dict_filter(f, d):
if not d: if not d:
return False return False
@ -129,8 +135,11 @@ def _get_entity(func, name_or_id, filters, **kwargs):
and returns a list of entities to filter. and returns a list of entities to filter.
:param string name_or_id: :param string name_or_id:
The name or ID of the entity being filtered or a dict The name or ID of the entity being filtered or a dict
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. A dictionary of meta data to use for further filtering.
OR
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 # 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 # fetched. Rather than then needing to pull the name or id out of that

View File

@ -464,7 +464,7 @@ class OpenStackCloud(object):
operators are one of: <,>,<=,>= operators are one of: <,>,<=,>=
:param list data: List of dictionaries to be searched. :param list data: List of dictionaries to be searched.
:param dict filters: Dict describing the one or more range searches to :param filters: Dict describing the one or more range searches to
perform. If more than one search is given, the result will be the perform. If more than one search is given, the result will be the
members of the original data set that match ALL searches. An members of the original data set that match ALL searches. An
example of filtering by multiple ranges:: example of filtering by multiple ranges::
@ -634,7 +634,10 @@ class OpenStackCloud(object):
"""Seach Keystone users. """Seach Keystone users.
:param string name: user name or id. :param string name: user name or id.
:param dict filters: a dict containing additional filters to use. :param filters: a dict containing additional filters to use.
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: a list of ``munch.Munch`` containing the users :returns: a list of ``munch.Munch`` containing the users
@ -648,7 +651,10 @@ class OpenStackCloud(object):
"""Get exactly one Keystone user. """Get exactly one Keystone user.
:param string name_or_id: user name or id. :param string name_or_id: user name or id.
:param dict filters: a dict containing additional filters to use. :param filters: a dict containing additional filters to use.
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: a single ``munch.Munch`` containing the user description. :returns: a single ``munch.Munch`` containing the user description.
@ -1939,7 +1945,7 @@ class OpenStackCloud(object):
"""Get a keypair by name or ID. """Get a keypair by name or ID.
:param name_or_id: Name or ID of the keypair. :param name_or_id: Name or ID of the keypair.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -1949,6 +1955,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A keypair ``munch.Munch`` or None if no matching keypair is :returns: A keypair ``munch.Munch`` or None if no matching keypair is
found. found.
@ -1960,7 +1969,7 @@ class OpenStackCloud(object):
"""Get a network by name or ID. """Get a network by name or ID.
:param name_or_id: Name or ID of the network. :param name_or_id: Name or ID of the network.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -1970,6 +1979,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A network ``munch.Munch`` or None if no matching network is :returns: A network ``munch.Munch`` or None if no matching network is
found. found.
@ -1981,7 +1993,7 @@ class OpenStackCloud(object):
"""Get a router by name or ID. """Get a router by name or ID.
:param name_or_id: Name or ID of the router. :param name_or_id: Name or ID of the router.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -1991,6 +2003,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A router ``munch.Munch`` or None if no matching router is :returns: A router ``munch.Munch`` or None if no matching router is
found. found.
@ -2002,7 +2017,7 @@ class OpenStackCloud(object):
"""Get a subnet by name or ID. """Get a subnet by name or ID.
:param name_or_id: Name or ID of the subnet. :param name_or_id: Name or ID of the subnet.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2023,7 +2038,7 @@ class OpenStackCloud(object):
"""Get a port by name or ID. """Get a port by name or ID.
:param name_or_id: Name or ID of the port. :param name_or_id: Name or ID of the port.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2033,6 +2048,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A port ``munch.Munch`` or None if no matching port is found. :returns: A port ``munch.Munch`` or None if no matching port is found.
@ -2043,7 +2061,7 @@ class OpenStackCloud(object):
"""Get a volume by name or ID. """Get a volume by name or ID.
:param name_or_id: Name or ID of the volume. :param name_or_id: Name or ID of the volume.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2053,6 +2071,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A volume ``munch.Munch`` or None if no matching volume is :returns: A volume ``munch.Munch`` or None if no matching volume is
found. found.
@ -2064,7 +2085,7 @@ class OpenStackCloud(object):
"""Get a flavor by name or ID. """Get a flavor by name or ID.
:param name_or_id: Name or ID of the flavor. :param name_or_id: Name or ID of the flavor.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2074,6 +2095,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:param get_extra: :param get_extra:
Whether or not the list_flavors call should get the extra flavor Whether or not the list_flavors call should get the extra flavor
specs. specs.
@ -2091,7 +2115,7 @@ class OpenStackCloud(object):
"""Get a security group by name or ID. """Get a security group by name or ID.
:param name_or_id: Name or ID of the security group. :param name_or_id: Name or ID of the security group.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2101,6 +2125,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A security group ``munch.Munch`` or None if no matching :returns: A security group ``munch.Munch`` or None if no matching
security group is found. security group is found.
@ -2147,7 +2174,7 @@ class OpenStackCloud(object):
"""Get a server by name or ID. """Get a server by name or ID.
:param name_or_id: Name or ID of the server. :param name_or_id: Name or ID of the server.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2157,6 +2184,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A server ``munch.Munch`` or None if no matching server is :returns: A server ``munch.Munch`` or None if no matching server is
found. found.
@ -2175,13 +2205,16 @@ class OpenStackCloud(object):
"""Get a server group by name or ID. """Get a server group by name or ID.
:param name_or_id: Name or ID of the server group. :param name_or_id: Name or ID of the server group.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
{ {
'policy': 'affinity', 'policy': 'affinity',
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A server groups dict or None if no matching server group :returns: A server groups dict or None if no matching server group
is found. is found.
@ -2194,7 +2227,7 @@ class OpenStackCloud(object):
"""Get an image by name or ID. """Get an image by name or ID.
:param name_or_id: Name or ID of the image. :param name_or_id: Name or ID of the image.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2204,6 +2237,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: An image ``munch.Munch`` or None if no matching image :returns: An image ``munch.Munch`` or None if no matching image
is found is found
@ -2255,7 +2291,7 @@ class OpenStackCloud(object):
"""Get a floating IP by ID """Get a floating IP by ID
:param id: ID of the floating IP. :param id: ID of the floating IP.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -2265,6 +2301,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A floating IP ``munch.Munch`` or None if no matching floating :returns: A floating IP ``munch.Munch`` or None if no matching floating
IP is found. IP is found.
@ -3395,7 +3434,7 @@ class OpenStackCloud(object):
"""Get a volume by name or ID. """Get a volume by name or ID.
:param name_or_id: Name or ID of the volume snapshot. :param name_or_id: Name or ID of the volume snapshot.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -3405,6 +3444,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A volume ``munch.Munch`` or None if no matching volume is :returns: A volume ``munch.Munch`` or None if no matching volume is
found. found.
@ -5922,8 +5964,11 @@ class OpenStackCloud(object):
"""Get a zone by name or ID. """Get a zone by name or ID.
:param name_or_id: Name or ID of the zone :param name_or_id: Name or ID of the zone
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering A dictionary of meta data to use for further filtering
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A zone dict or None if no matching zone is :returns: A zone dict or None if no matching zone is
found. found.
@ -6187,7 +6232,7 @@ class OpenStackCloud(object):
ClusterTemplate is the new name for BayModel. ClusterTemplate is the new name for BayModel.
:param name_or_id: Name or ID of the ClusterTemplate. :param name_or_id: Name or ID of the ClusterTemplate.
:param dict filters: :param filters:
A dictionary of meta data to use for further filtering. Elements A dictionary of meta data to use for further filtering. Elements
of this dictionary may, themselves, be dictionaries. Example:: of this dictionary may, themselves, be dictionaries. Example::
@ -6197,6 +6242,9 @@ class OpenStackCloud(object):
'gender': 'Female' 'gender': 'Female'
} }
} }
OR
A string containing a jmespath expression for further filtering.
Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
:returns: A ClusterTemplate dict or None if no matching :returns: A ClusterTemplate dict or None if no matching
ClusterTemplate is found. ClusterTemplate is found.

View File

@ -67,6 +67,10 @@ class TestUsers(base.BaseFunctionalTestCase):
users = self.operator_cloud.search_users(filters={'enabled': True}) users = self.operator_cloud.search_users(filters={'enabled': True})
self.assertIsNotNone(users) self.assertIsNotNone(users)
def test_search_users_jmespath(self):
users = self.operator_cloud.search_users(filters="[?enabled]")
self.assertIsNotNone(users)
def test_create_user(self): def test_create_user(self):
user_name = self.user_prefix + '_create' user_name = self.user_prefix + '_create'
user_email = 'nobody@nowhere.com' user_email = 'nobody@nowhere.com'

View File

@ -45,6 +45,13 @@ class TestUtils(base.TestCase):
ret = _utils._filter_list(data, 'donald', {'other': 'duck'}) ret = _utils._filter_list(data, 'donald', {'other': 'duck'})
self.assertEqual([el1], ret) self.assertEqual([el1], ret)
def test__filter_list_filter_jmespath(self):
el1 = dict(id=100, name='donald', other='duck')
el2 = dict(id=200, name='donald', other='trump')
data = [el1, el2]
ret = _utils._filter_list(data, 'donald', "[?other == `duck`]")
self.assertEqual([el1], ret)
def test__filter_list_dict1(self): def test__filter_list_dict1(self):
el1 = dict(id=100, name='donald', last='duck', el1 = dict(id=100, name='donald', last='duck',
other=dict(category='duck')) other=dict(category='duck'))