Support generalized resource filter in client

Introduce new command 'list-filters' to retrieve
enabled resource filters.

```
command: cinder list-filters --resource=volume

output:
+----------------+-------------------------------+
| Resource       | Filters                       |
+----------------+-------------------------------+
| volume         | name, status, image_metadata  |
+----------------+-------------------------------+

```

Also Added new option '--filters' to these list commands:
1. list
2. snapshot-list
3. backup-list
4. attachment-list
5. message-list
6. group-list
7. group-snapshot-list

Change-Id: I062e6227342ea0d940a8333e84014969c33b49df
Partial: blueprint generalized-filtering-for-cinder-list-resource
Depends-On: 7fdc4688fea373afb85d929e649d311568d1855a
This commit is contained in:
TommyLike
2017-05-16 17:22:08 +08:00
parent 16af9f72b6
commit 93b1c11349
10 changed files with 428 additions and 25 deletions

View File

@@ -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 = {}

View File

@@ -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 = {}

View File

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

View File

@@ -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',

View File

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

View File

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

View File

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

View File

@@ -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 "<ResourceFilter: %s>" % 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")

View File

@@ -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='<resource>',
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='<all_tenants>',
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='<name>',
default=None,
help="Filters results by a name. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--status',
metavar='<status>',
default=None,
help="Filters results by a status. Default=None. "
"%s" % FILTER_DEPRECATED)
@utils.arg('--volume-id',
metavar='<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',
metavar='<marker>',
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='<limit>',
default=None,
help='Maximum number of backups to return. Default=None.')
@utils.arg('--sort',
metavar='<key>[:<direction>]',
default=None,
help=(('Comma-separated list of sort keys and directions in the '
'form of <key>[:<asc|desc>]. '
'Valid keys: %s. '
'Default=None.') % ', '.join(base.SORT_KEY_VALUES)))
@utils.arg('--filters',
type=str,
nargs='*',
start_version='3.32',
metavar='<key=value>',
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='<group_id>',
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='<name>',
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='<status>',
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='<True|true|False|false>',
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='<migration_status>',
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='<key=value>',
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='<key=value>',
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='<marker>',
default=None,
@@ -125,6 +227,14 @@ from cinderclient.v2.shell import * # flake8: noqa
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.arg('--filters',
type=str,
nargs='*',
start_version='3.32',
metavar='<key=value>',
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='<key=value>',
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='<status>',
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='<group_id>',
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='<key=value>',
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='<resource_uuid>',
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='<type>',
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='<id>',
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='<request_id>',
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='<level>',
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='<key=value>',
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='<name>',
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='<status>',
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='<volume-id>',
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='<key=value>',
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='<key=value>',
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='<volume-id>',
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='<status>',
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='<marker>',
default=None,
@@ -1344,6 +1516,14 @@ def do_snapshot_list(cs, args):
nargs='?',
metavar='<tenant>',
help='Display information from single tenant (Admin only).')
@utils.arg('--filters',
type=str,
nargs='*',
start_version='3.32',
metavar='<key=value>',
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,

View File

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