diff --git a/cinderclient/api_versions.py b/cinderclient/api_versions.py index dee7c78b1..163aa2259 100644 --- a/cinderclient/api_versions.py +++ b/cinderclient/api_versions.py @@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__) # key is a deprecated version and value is an alternative version. DEPRECATED_VERSIONS = {"1": "2"} DEPRECATED_VERSION = "2.0" -MAX_VERSION = "3.28" +MAX_VERSION = "3.32" MIN_VERSION = "3.0" _SUBSTITUTIONS = {} diff --git a/cinderclient/shell_utils.py b/cinderclient/shell_utils.py index e38655881..11b7ccc5f 100644 --- a/cinderclient/shell_utils.py +++ b/cinderclient/shell_utils.py @@ -143,6 +143,26 @@ def translate_availability_zone_keys(collection): translate_keys(collection, convert) +def extract_filters(args): + filters = {} + for filter in args: + if '=' in filter: + (key, value) = filter.split('=', 1) + if value.startswith('{') and value.endswith('}'): + value = _build_internal_dict(value[1:-1]) + filters[key] = value + + return filters + + +def _build_internal_dict(content): + result = {} + for pair in content.split(','): + k, v = pair.split(':', 1) + result.update({k.strip(): v.strip()}) + return result + + def extract_metadata(args, type='user_metadata'): metadata = {} if type == 'image_metadata': @@ -169,6 +189,11 @@ def print_group_type_list(gtypes): utils.print_list(gtypes, ['ID', 'Name', 'Description']) +def print_resource_filter_list(filters): + formatter = {'Filters': lambda resource: ', '.join(resource.filters)} + utils.print_list(filters, ['Resource', 'Filters'], formatters=formatter) + + def quota_show(quotas): quotas_info_dict = utils.unicode_key_value_to_string(quotas._info) quota_dict = {} diff --git a/cinderclient/tests/unit/test_utils.py b/cinderclient/tests/unit/test_utils.py index a62425e4a..eeb480037 100644 --- a/cinderclient/tests/unit/test_utils.py +++ b/cinderclient/tests/unit/test_utils.py @@ -12,6 +12,7 @@ # limitations under the License. import collections +import ddt import sys import mock @@ -21,6 +22,7 @@ import six from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import exceptions +from cinderclient import shell_utils from cinderclient import utils from cinderclient import base from cinderclient.tests.unit import utils as test_utils @@ -187,6 +189,21 @@ class BuildQueryParamTestCase(test_utils.TestCase): self.assertFalse(result_2) +@ddt.ddt +class ExtractFilterTestCase(test_utils.TestCase): + + @ddt.data({'content': ['key1=value1'], + 'expected': {'key1': 'value1'}}, + {'content': ['key1={key2:value2}'], + 'expected': {'key1': {'key2': 'value2'}}}, + {'content': ['key1=value1', 'key2={key22:value22}'], + 'expected': {'key1': 'value1', 'key2': {'key22': 'value22'}}}) + @ddt.unpack + def test_extract_filters(self, content, expected): + result = shell_utils.extract_filters(content) + self.assertEqual(expected, result) + + class PrintListTestCase(test_utils.TestCase): def test_print_list_with_list(self): diff --git a/cinderclient/tests/unit/v3/fakes.py b/cinderclient/tests/unit/v3/fakes.py index e53aac7e6..9af49df31 100644 --- a/cinderclient/tests/unit/v3/fakes.py +++ b/cinderclient/tests/unit/v3/fakes.py @@ -499,6 +499,12 @@ class FakeHTTPClient(fake_v2.FakeHTTPClient): } return 200, {}, {'message': message} + # + # resource filters + # + def get_resource_filters(self, **kw): + return 200, {}, {'resource_filters': []} + def fake_request_get(): versions = {'versions': [{'id': 'v1.0', diff --git a/cinderclient/tests/unit/v3/test_resource_filters.py b/cinderclient/tests/unit/v3/test_resource_filters.py new file mode 100644 index 000000000..3b141240d --- /dev/null +++ b/cinderclient/tests/unit/v3/test_resource_filters.py @@ -0,0 +1,32 @@ +# 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. + +import ddt + +from cinderclient.tests.unit import utils +from cinderclient.tests.unit.v3 import fakes + +cs = fakes.FakeClient() + + +@ddt.ddt +class ResourceFilterTests(utils.TestCase): + @ddt.data({'resource': None, 'query_url': None}, + {'resource': 'volume', 'query_url': '?resource=volume'}, + {'resource': 'group', 'query_url': '?resource=group'}) + @ddt.unpack + def test_list_messages(self, resource, query_url): + cs.resource_filters.list(resource) + url = '/resource_filters' + if resource is not None: + url += query_url + cs.assert_called('GET', url) diff --git a/cinderclient/tests/unit/v3/test_shell.py b/cinderclient/tests/unit/v3/test_shell.py index 70dc72602..dec735843 100644 --- a/cinderclient/tests/unit/v3/test_shell.py +++ b/cinderclient/tests/unit/v3/test_shell.py @@ -66,6 +66,95 @@ class ShellTest(utils.TestCase): return self.shell.cs.assert_called(method, url, body, partial_body, **kwargs) + @ddt.data({'resource': None, 'query_url': None}, + {'resource': 'volume', 'query_url': '?resource=volume'}, + {'resource': 'group', 'query_url': '?resource=group'}) + @ddt.unpack + def test_list_filters(self, resource, query_url): + url = '/resource_filters' + if resource is not None: + url += query_url + self.run_command('--os-volume-api-version 3.32 ' + 'list-filters --resource=%s' % resource) + else: + self.run_command('--os-volume-api-version 3.32 list-filters') + + self.assert_called('GET', url) + + @ddt.data( + # testcases for list volume + {'command': + 'list --name=123 --filters name=456', + 'expected': + '/volumes/detail?name=456'}, + {'command': + 'list --filters name=123', + 'expected': + '/volumes/detail?name=123'}, + {'command': + 'list --filters metadata={key1:value1}', + 'expected': + '/volumes/detail?metadata=%7B%27key1%27%3A+%27value1%27%7D'}, + # testcases for list group + {'command': + 'group-list --filters name=456', + 'expected': + '/groups/detail?name=456'}, + {'command': + 'group-list --filters status=available', + 'expected': + '/groups/detail?status=available'}, + # testcases for list group-snapshot + {'command': + 'group-snapshot-list --status=error --filters status=available', + 'expected': + '/group_snapshots/detail?status=available'}, + {'command': + 'group-snapshot-list --filters availability_zone=123', + 'expected': + '/group_snapshots/detail?availability_zone=123'}, + # testcases for list message + {'command': + 'message-list --event_id=123 --filters event_id=456', + 'expected': + '/messages?event_id=456'}, + {'command': + 'message-list --filters request_id=123', + 'expected': + '/messages?request_id=123'}, + # testcases for list attachment + {'command': + 'attachment-list --volume-id=123 --filters volume_id=456', + 'expected': + '/attachments?volume_id=456'}, + {'command': + 'attachment-list --filters mountpoint=123', + 'expected': + '/attachments?mountpoint=123'}, + # testcases for list backup + {'command': + 'backup-list --volume-id=123 --filters volume_id=456', + 'expected': + '/backups/detail?volume_id=456'}, + {'command': + 'backup-list --filters name=123', + 'expected': + '/backups/detail?name=123'}, + # testcases for list snapshot + {'command': + 'snapshot-list --volume-id=123 --filters volume_id=456', + 'expected': + '/snapshots/detail?volume_id=456'}, + {'command': + 'snapshot-list --filters name=123', + 'expected': + '/snapshots/detail?name=123'}, + ) + @ddt.unpack + def test_list_with_filters_mixed(self, command, expected): + self.run_command('--os-volume-api-version 3.32 %s' % command) + self.assert_called('GET', expected) + def test_list(self): self.run_command('list') # NOTE(jdg): we default to detail currently diff --git a/cinderclient/v3/client.py b/cinderclient/v3/client.py index 0d4bb86cb..ef242f4bb 100644 --- a/cinderclient/v3/client.py +++ b/cinderclient/v3/client.py @@ -32,6 +32,7 @@ from cinderclient.v3 import pools from cinderclient.v3 import qos_specs from cinderclient.v3 import quota_classes from cinderclient.v3 import quotas +from cinderclient.v3 import resource_filters from cinderclient.v3 import services from cinderclient.v3 import volumes from cinderclient.v3 import volume_snapshots @@ -85,6 +86,7 @@ class Client(object): self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) self.messages = messages.MessageManager(self) + self.resource_filters = resource_filters.ResourceFilterManager(self) self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) self.services = services.ServiceManager(self) diff --git a/cinderclient/v3/resource_filters.py b/cinderclient/v3/resource_filters.py new file mode 100644 index 000000000..fa9c15d76 --- /dev/null +++ b/cinderclient/v3/resource_filters.py @@ -0,0 +1,37 @@ +# 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. + +"""Resource filters interface.""" + +from cinderclient import base +from cinderclient import api_versions + + +class ResourceFilter(base.Resource): + NAME_ATTR = 'resource' + + def __repr__(self): + return "" % self.resource + + +class ResourceFilterManager(base.ManagerWithFind): + """Manage :class:`ResourceFilter` resources.""" + + resource_class = ResourceFilter + + @api_versions.wraps('3.32') + def list(self, resource): + """List all resource filters.""" + url = '/resource_filters' + if resource is not None: + url += '?resource=%s' % resource + return self._list(url, "resource_filters") diff --git a/cinderclient/v3/shell.py b/cinderclient/v3/shell.py index f539dc59d..6de0b930f 100644 --- a/cinderclient/v3/shell.py +++ b/cinderclient/v3/shell.py @@ -32,11 +32,107 @@ from cinderclient import utils from cinderclient.v2.shell import * # flake8: noqa +FILTER_DEPRECATED = ("This option is deprecated and will be rejected in " + "newer release, please use '--filters' option instead.") + + +@api_versions.wraps('3.32') +@utils.arg('--resource', + metavar='', + default=None, + help='Show enabled filters for specified resource. Default=None.') +def do_list_filters(cs, args): + filters = cs.resource_filters.list(args.resource) + shell_utils.print_resource_filter_list(filters) + + +@utils.arg('--all-tenants', + metavar='', + nargs='?', + type=int, + const=1, + default=0, + help='Shows details for all tenants. Admin only.') +@utils.arg('--all_tenants', + nargs='?', + type=int, + const=1, + help=argparse.SUPPRESS) +@utils.arg('--name', + metavar='', + default=None, + help="Filters results by a name. Default=None. " + "%s" % FILTER_DEPRECATED) +@utils.arg('--status', + metavar='', + default=None, + help="Filters results by a status. Default=None. " + "%s" % FILTER_DEPRECATED) +@utils.arg('--volume-id', + metavar='', + default=None, + help="Filters results by a volume ID. Default=None. " + "%s" % FILTER_DEPRECATED) +@utils.arg('--volume_id', + help=argparse.SUPPRESS) +@utils.arg('--marker', + metavar='', + default=None, + help='Begin returning backups that appear later in the backup ' + 'list than that represented by this id. ' + 'Default=None.') +@utils.arg('--limit', + metavar='', + default=None, + help='Maximum number of backups to return. Default=None.') +@utils.arg('--sort', + metavar='[:]', + default=None, + help=(('Comma-separated list of sort keys and directions in the ' + 'form of [:]. ' + 'Valid keys: %s. ' + 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) +@utils.arg('--filters', + type=str, + nargs='*', + start_version='3.32', + metavar='', + default=None, + help="Filter key and value pairs. Please use 'cinder list-filters' " + "to check enabled filters from server, Default=None.") +def do_backup_list(cs, args): + """Lists all backups.""" + + search_opts = { + 'all_tenants': args.all_tenants, + 'name': args.name, + 'status': args.status, + 'volume_id': args.volume_id, + } + + # Update search option with `filters` + if hasattr(args, 'filters') and args.filters is not None: + search_opts.update(shell_utils.extract_filters(args.filters)) + + backups = cs.backups.list(search_opts=search_opts, + marker=args.marker, + limit=args.limit, + sort=args.sort) + shell_utils.translate_volume_snapshot_keys(backups) + columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', + 'Container'] + if args.sort: + sortby_index = None + else: + sortby_index = 0 + utils.print_list(backups, columns, sortby_index=sortby_index) + @utils.arg('--group_id', metavar='', default=None, - help='Filters results by a group_id. Default=None.', + help="Filters results by a group_id. Default=None." + "%s" % FILTER_DEPRECATED, start_version='3.10') @utils.arg('--all-tenants', dest='all_tenants', @@ -54,39 +150,45 @@ from cinderclient.v2.shell import * # flake8: noqa @utils.arg('--name', metavar='', default=None, - help='Filters results by a name. Default=None.') + help="Filters results by a name. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--status', metavar='', default=None, - help='Filters results by a status. Default=None.') + help="Filters results by a status. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--bootable', metavar='', const=True, nargs='?', choices=['True', 'true', 'False', 'false'], - help='Filters results by bootable status. Default=None.') + help="Filters results by bootable status. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--migration_status', metavar='', default=None, - help='Filters results by a migration status. Default=None. ' - 'Admin only.') + help="Filters results by a migration status. Default=None. " + "Admin only. " + "%s" % FILTER_DEPRECATED) @utils.arg('--metadata', type=str, nargs='*', metavar='', default=None, - help='Filters results by a metadata key and value pair. ' - 'Default=None.') + help="Filters results by a metadata key and value pair. " + "Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--image_metadata', type=str, nargs='*', metavar='', default=None, start_version='3.4', - help='Filters results by a image metadata key and value pair. Require ' - 'volume api version >=3.4. Default=None.') + help="Filters results by a image metadata key and value pair. " + "Require volume api version >=3.4. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--marker', metavar='', default=None, @@ -125,6 +227,14 @@ from cinderclient.v2.shell import * # flake8: noqa nargs='?', metavar='', help='Display information from single tenant (Admin only).') +@utils.arg('--filters', + type=str, + nargs='*', + start_version='3.32', + metavar='', + default=None, + help="Filter key and value pairs. Please use 'cinder list-filters' " + "to check enabled filters from server, Default=None.") def do_list(cs, args): """Lists all volumes.""" # NOTE(thingee): Backwards-compatibility with v1 args @@ -147,6 +257,9 @@ def do_list(cs, args): if hasattr(args, 'image_metadata') and args.image_metadata else None, 'group_id': getattr(args, 'group_id', None), } + # Update search option with `filters` + if hasattr(args, 'filters') and args.filters is not None: + search_opts.update(shell_utils.extract_filters(args.filters)) # If unavailable/non-existent fields are specified, these fields will # be removed from key_list at the print_list() during key validation. @@ -755,10 +868,22 @@ def do_manageable_list(cs, args): const=1, default=utils.env('ALL_TENANTS', default=0), help='Shows details for all tenants. Admin only.') +@utils.arg('--filters', + type=str, + nargs='*', + start_version='3.32', + metavar='', + default=None, + help="Filter key and value pairs. Please use 'cinder list-filters' " + "to check enabled filters from server, Default=None.") def do_group_list(cs, args): """Lists all groups.""" search_opts = {'all_tenants': args.all_tenants} + # Update search option with `filters` + if hasattr(args, 'filters') and args.filters is not None: + search_opts.update(shell_utils.extract_filters(args.filters)) + groups = cs.groups.list(search_opts=search_opts) columns = ['ID', 'Status', 'Name'] @@ -938,11 +1063,21 @@ def do_group_update(cs, args): @utils.arg('--status', metavar='', default=None, - help='Filters results by a status. Default=None.') + help="Filters results by a status. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--group-id', metavar='', default=None, - help='Filters results by a group ID. Default=None.') + help="Filters results by a group ID. Default=None. " + "%s" % FILTER_DEPRECATED) +@utils.arg('--filters', + type=str, + nargs='*', + start_version='3.32', + metavar='', + default=None, + help="Filter key and value pairs. Please use 'cinder list-filters' " + "to check enabled filters from server, Default=None.") def do_group_snapshot_list(cs, args): """Lists all group snapshots.""" @@ -953,6 +1088,9 @@ def do_group_snapshot_list(cs, args): 'status': args.status, 'group_id': args.group_id, } + # Update search option with `filters` + if hasattr(args, 'filters') and args.filters is not None: + search_opts.update(shell_utils.extract_filters(args.filters)) group_snapshots = cs.group_snapshots.list(search_opts=search_opts) @@ -1131,23 +1269,36 @@ def do_api_version(cs, args): @utils.arg('--resource_uuid', metavar='', default=None, - help='Filters results by a resource uuid. Default=None.') + help="Filters results by a resource uuid. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--resource_type', metavar='', default=None, - help='Filters results by a resource type. Default=None.') + help="Filters results by a resource type. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--event_id', metavar='', default=None, - help='Filters results by event id. Default=None.') + help="Filters results by event id. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--request_id', metavar='', default=None, - help='Filters results by request id. Default=None.') + help="Filters results by request id. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--level', metavar='', default=None, - help='Filters results by the message level. Default=None.') + help="Filters results by the message level. Default=None. " + "%s" % FILTER_DEPRECATED) +@utils.arg('--filters', + type=str, + nargs='*', + start_version='3.32', + metavar='', + default=None, + help="Filter key and value pairs. Please use 'cinder list-filters' " + "to check enabled filters from server, Default=None.") def do_message_list(cs, args): """Lists all messages.""" search_opts = { @@ -1155,6 +1306,9 @@ def do_message_list(cs, args): 'event_id': args.event_id, 'request_id': args.request_id, } + # Update search option with `filters` + if hasattr(args, 'filters') and args.filters is not None: + search_opts.update(shell_utils.extract_filters(args.filters)) if args.resource_type: search_opts['resource_type'] = args.resource_type.upper() if args.level: @@ -1224,7 +1378,8 @@ def do_message_delete(cs, args): @utils.arg('--name', metavar='', default=None, - help='Filters results by a name. Default=None.') + help="Filters results by a name. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--display_name', @@ -1232,11 +1387,13 @@ def do_message_delete(cs, args): @utils.arg('--status', metavar='', default=None, - help='Filters results by a status. Default=None.') + help="Filters results by a status. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--volume-id', metavar='', default=None, - help='Filters results by a volume ID. Default=None.') + help="Filters results by a volume ID. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--volume_id', help=argparse.SUPPRESS) @utils.arg('--marker', @@ -1267,8 +1424,17 @@ def do_message_delete(cs, args): metavar='', default=None, start_version='3.22', - help='Filters results by a metadata key and value pair. Require ' - 'volume api version >=3.22. Default=None.') + help="Filters results by a metadata key and value pair. Require " + "volume api version >=3.22. Default=None. " + "%s" % FILTER_DEPRECATED) +@utils.arg('--filters', + type=str, + nargs='*', + start_version='3.32', + metavar='', + default=None, + help="Filter key and value pairs. Please use 'cinder list-filters' " + "to check enabled filters from server, Default=None.") def do_snapshot_list(cs, args): """Lists all snapshots.""" all_tenants = (1 if args.tenant else @@ -1293,6 +1459,10 @@ def do_snapshot_list(cs, args): 'metadata': metadata } + # Update search option with `filters` + if hasattr(args, 'filters') and args.filters is not None: + search_opts.update(shell_utils.extract_filters(args.filters)) + snapshots = cs.volume_snapshots.list(search_opts=search_opts, marker=args.marker, limit=args.limit, @@ -1316,11 +1486,13 @@ def do_snapshot_list(cs, args): @utils.arg('--volume-id', metavar='', default=None, - help='Filters results by a volume ID. Default=None.') + help="Filters results by a volume ID. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--status', metavar='', default=None, - help='Filters results by a status. Default=None.') + help="Filters results by a status. Default=None. " + "%s" % FILTER_DEPRECATED) @utils.arg('--marker', metavar='', default=None, @@ -1344,6 +1516,14 @@ def do_snapshot_list(cs, args): nargs='?', metavar='', help='Display information from single tenant (Admin only).') +@utils.arg('--filters', + type=str, + nargs='*', + start_version='3.32', + metavar='', + default=None, + help="Filter key and value pairs. Please use 'cinder list-filters' " + "to check enabled filters from server, Default=None.") def do_attachment_list(cs, args): """Lists all attachments.""" search_opts = { @@ -1352,6 +1532,9 @@ def do_attachment_list(cs, args): 'status': args.status, 'volume_id': args.volume_id, } + # Update search option with `filters` + if hasattr(args, 'filters') and args.filters is not None: + search_opts.update(shell_utils.extract_filters(args.filters)) attachments = cs.attachments.list(search_opts=search_opts, marker=args.marker, diff --git a/releasenotes/notes/support-generialized-resource-filter-8yf6w23f66bf5903.yaml b/releasenotes/notes/support-generialized-resource-filter-8yf6w23f66bf5903.yaml new file mode 100644 index 000000000..f0ad9b464 --- /dev/null +++ b/releasenotes/notes/support-generialized-resource-filter-8yf6w23f66bf5903.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added new command ``list-filters`` to retrieve enabled resource filters, + Added new option ``--filters`` to these list commands: + - list + - snapshot-list + - backup-list + - group-list + - group-snapshot-list + - attachment-list + - message-list