Handle empty metadata properly in filter/list
Introduced "None" tag for metadata command. "list --metadata None" command will filter out shares that has empty metadata, i.e., metadata={} Closes-Bug: #1782847 Change-Id: Iac33cd1295b1e7c2ff11e6d7bd003fde28785c0e
This commit is contained in:
parent
e12db73f85
commit
d24527a7f4
@ -224,12 +224,15 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
|
|||||||
|
|
||||||
def get_shares_detail(self, **kw):
|
def get_shares_detail(self, **kw):
|
||||||
endpoint = "http://127.0.0.1:8786/v2"
|
endpoint = "http://127.0.0.1:8786/v2"
|
||||||
share_id = '1234'
|
share_id1234 = '1234'
|
||||||
|
share_id1111 = '1111'
|
||||||
|
share_id2222 = '2222'
|
||||||
|
share_id3333 = '3333'
|
||||||
|
|
||||||
shares = {
|
shares = {
|
||||||
'shares': [
|
'shares': [
|
||||||
{
|
{
|
||||||
'id': share_id,
|
'id': share_id1234,
|
||||||
'name': 'sharename',
|
'name': 'sharename',
|
||||||
'status': 'fake_status',
|
'status': 'fake_status',
|
||||||
'size': 1,
|
'size': 1,
|
||||||
@ -237,9 +240,52 @@ class FakeHTTPClient(fakes.FakeHTTPClient):
|
|||||||
'export_location': 'fake_export_location',
|
'export_location': 'fake_export_location',
|
||||||
'snapshot_id': 'fake_snapshot_id',
|
'snapshot_id': 'fake_snapshot_id',
|
||||||
'links': [
|
'links': [
|
||||||
{"href": endpoint + "/fake_project/shares/" + share_id,
|
{"href": endpoint + "/fake_project/shares/"
|
||||||
"rel": "self"},
|
+ share_id1234, "rel": "self"},
|
||||||
],
|
],
|
||||||
|
'metadata': {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': share_id1111,
|
||||||
|
'name': 'sharename1',
|
||||||
|
'status': 'fake_status1',
|
||||||
|
'size': 1,
|
||||||
|
'host': 'fake_host',
|
||||||
|
'export_location': 'fake_export_location',
|
||||||
|
'snapshot_id': 'fake_snapshot_id',
|
||||||
|
'links': [
|
||||||
|
{"href": endpoint + "/fake_project/shares/"
|
||||||
|
+ share_id1111, "rel": "self"},
|
||||||
|
],
|
||||||
|
'metadata': {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': share_id2222,
|
||||||
|
'name': 'sharename2',
|
||||||
|
'status': 'fake_status2',
|
||||||
|
'size': 1,
|
||||||
|
'host': 'fake_host',
|
||||||
|
'export_location': 'fake_export_location',
|
||||||
|
'snapshot_id': 'fake_snapshot_id',
|
||||||
|
'links': [
|
||||||
|
{"href": endpoint + "/fake_project/shares/"
|
||||||
|
+ share_id2222, "rel": "self"},
|
||||||
|
],
|
||||||
|
'metadata': {'key1': 'value1'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': share_id3333,
|
||||||
|
'name': 'sharename3',
|
||||||
|
'status': 'fake_status3',
|
||||||
|
'size': 1,
|
||||||
|
'host': 'fake_host',
|
||||||
|
'export_location': 'fake_export_location',
|
||||||
|
'snapshot_id': 'fake_snapshot_id',
|
||||||
|
'links': [
|
||||||
|
{"href": endpoint + "/fake_project/shares/"
|
||||||
|
+ share_id3333, "rel": "self"},
|
||||||
|
],
|
||||||
|
'metadata': {'key1': 'value1', 'key2': 'value2'}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -421,7 +421,7 @@ class SharesTest(utils.TestCase):
|
|||||||
cs.assert_called(
|
cs.assert_called(
|
||||||
'GET', '/shares/detail?is_public=True&with_count=True')
|
'GET', '/shares/detail?is_public=True&with_count=True')
|
||||||
self.assertEqual(2, count)
|
self.assertEqual(2, count)
|
||||||
self.assertEqual(1, len(shares))
|
self.assertEqual(4, len(shares))
|
||||||
|
|
||||||
def test_list_shares_detailed_with_count(self):
|
def test_list_shares_detailed_with_count(self):
|
||||||
cs.shares.list(detailed=True)
|
cs.shares.list(detailed=True)
|
||||||
|
@ -231,8 +231,65 @@ class ShellTest(test_utils.TestCase):
|
|||||||
|
|
||||||
def test_list_filter_by_metadata(self):
|
def test_list_filter_by_metadata(self):
|
||||||
self.run_command('list --metadata key=value')
|
self.run_command('list --metadata key=value')
|
||||||
|
# /shares/detail?metadata={'key': 'value'}
|
||||||
self.assert_called(
|
self.assert_called(
|
||||||
'GET', '/shares/detail?metadata=%7B%27key%27%3A+%27value%27%7D')
|
'GET', '/shares/detail?metadata='
|
||||||
|
'%7B%27key%27%3A+%27value%27%7D')
|
||||||
|
|
||||||
|
def test_list_filter_by_metadata_with_multiple_key_values(self):
|
||||||
|
self.run_command('list --metadata key1=value1 '
|
||||||
|
'key2=value2 key3=value3')
|
||||||
|
# /shares/detail?metadata={'key1': 'value1',
|
||||||
|
# 'key2': 'value2', 'key3': 'value3'}
|
||||||
|
self.assert_called(
|
||||||
|
'GET', '/shares/detail?metadata='
|
||||||
|
'%7B%27key1%27%3A+%27value1%27%2C+%27key2%27%3A+'
|
||||||
|
'%27value2%27%2C+%27key3%27%3A+%27value3%27%7D')
|
||||||
|
|
||||||
|
@mock.patch.object(cliutils, 'print_list', mock.Mock())
|
||||||
|
def test_list_filter_by_metadata_with_empty_metadata(self):
|
||||||
|
self.run_command('list --metadata \'\'')
|
||||||
|
|
||||||
|
self.assert_called(
|
||||||
|
'GET', '/shares/detail')
|
||||||
|
cliutils.print_list.assert_called()
|
||||||
|
args, _ = cliutils.print_list.call_args
|
||||||
|
shares = args[0]
|
||||||
|
# All 4 shares irrespective of metadata values printed
|
||||||
|
self.assertEqual(len(shares), 4)
|
||||||
|
|
||||||
|
@mock.patch.object(cliutils, 'print_list', mock.Mock())
|
||||||
|
def test_list_filter_by_metadata_set_to_None(self):
|
||||||
|
self.run_command('list --metadata None')
|
||||||
|
|
||||||
|
self.assert_called(
|
||||||
|
'GET', '/shares/detail')
|
||||||
|
cliutils.print_list.assert_called()
|
||||||
|
args, _ = cliutils.print_list.call_args
|
||||||
|
shares = args[0]
|
||||||
|
# Check that the size of shares is 2(shares with metadata={})
|
||||||
|
self.assertEqual(len(shares), 2)
|
||||||
|
for share in shares:
|
||||||
|
self.assertEqual(share.metadata, {})
|
||||||
|
|
||||||
|
def test_list_filter_by_metadata_with_one_empty_of_many_metadata(self):
|
||||||
|
self.run_command('list --metadata key=value \'\'')
|
||||||
|
# /shares/detail?metadata={'key': 'value'}
|
||||||
|
self.assert_called(
|
||||||
|
'GET', '/shares/detail?metadata='
|
||||||
|
'%7B%27key%27%3A+%27value%27%7D')
|
||||||
|
|
||||||
|
@mock.patch.object(cliutils, 'print_list', mock.Mock())
|
||||||
|
def test_list_filter_by_metadata_with_no_metadata(self):
|
||||||
|
self.run_command('list --metadata')
|
||||||
|
# /shares/detail
|
||||||
|
self.assert_called(
|
||||||
|
'GET', '/shares/detail')
|
||||||
|
cliutils.print_list.assert_called()
|
||||||
|
args, _ = cliutils.print_list.call_args
|
||||||
|
shares = args[0]
|
||||||
|
# All 4 shares printed
|
||||||
|
self.assertEqual(len(shares), 4)
|
||||||
|
|
||||||
def test_list_filter_by_extra_specs_and_its_aliases(self):
|
def test_list_filter_by_extra_specs_and_its_aliases(self):
|
||||||
aliases = ['--extra-specs', '--extra_specs', ]
|
aliases = ['--extra-specs', '--extra_specs', ]
|
||||||
|
@ -396,8 +396,8 @@ def _translate_keys(collection, convert):
|
|||||||
setattr(item, to_key, item._info[from_key])
|
setattr(item, to_key, item._info[from_key])
|
||||||
|
|
||||||
|
|
||||||
def _extract_metadata(args):
|
def _extract_metadata(args, allow_empty_key=True):
|
||||||
return _extract_key_value_options(args, 'metadata')
|
return _extract_key_value_options(args, 'metadata', allow_empty_key)
|
||||||
|
|
||||||
|
|
||||||
def _extract_extra_specs(args):
|
def _extract_extra_specs(args):
|
||||||
@ -408,7 +408,7 @@ def _extract_group_specs(args):
|
|||||||
return _extract_key_value_options(args, 'group_specs')
|
return _extract_key_value_options(args, 'group_specs')
|
||||||
|
|
||||||
|
|
||||||
def _extract_key_value_options(args, option_name):
|
def _extract_key_value_options(args, option_name, allow_empty_key=True):
|
||||||
result_dict = {}
|
result_dict = {}
|
||||||
duplicate_options = []
|
duplicate_options = []
|
||||||
|
|
||||||
@ -419,10 +419,12 @@ def _extract_key_value_options(args, option_name):
|
|||||||
# unset doesn't require a val, so we have the if/else
|
# unset doesn't require a val, so we have the if/else
|
||||||
if '=' in option:
|
if '=' in option:
|
||||||
(key, value) = option.split('=', 1)
|
(key, value) = option.split('=', 1)
|
||||||
else:
|
elif allow_empty_key:
|
||||||
key = option
|
key = option
|
||||||
value = None
|
value = None
|
||||||
|
else:
|
||||||
|
# disallow empty key
|
||||||
|
continue
|
||||||
if key not in result_dict:
|
if key not in result_dict:
|
||||||
result_dict[key] = value
|
result_dict[key] = value
|
||||||
else:
|
else:
|
||||||
@ -2502,7 +2504,7 @@ def do_list(cs, args):
|
|||||||
'share_network_id': share_network.id,
|
'share_network_id': share_network.id,
|
||||||
'snapshot_id': snapshot.id,
|
'snapshot_id': snapshot.id,
|
||||||
'share_type_id': share_type.id,
|
'share_type_id': share_type.id,
|
||||||
'metadata': _extract_metadata(args),
|
'metadata': _extract_metadata(args, allow_empty_key=False),
|
||||||
'extra_specs': _extract_extra_specs(args),
|
'extra_specs': _extract_extra_specs(args),
|
||||||
'share_server_id': args.share_server_id,
|
'share_server_id': args.share_server_id,
|
||||||
'project_id': args.project_id,
|
'project_id': args.project_id,
|
||||||
@ -2558,6 +2560,10 @@ def do_list(cs, args):
|
|||||||
search_opts=search_opts, sort_key=args.sort_key,
|
search_opts=search_opts, sort_key=args.sort_key,
|
||||||
sort_dir=args.sort_dir
|
sort_dir=args.sort_dir
|
||||||
)
|
)
|
||||||
|
# When shell input is "--metadata None", filter metadata={}
|
||||||
|
if shares and args.metadata:
|
||||||
|
if "None" in args.metadata:
|
||||||
|
shares = [share for share in shares if share.metadata == {}]
|
||||||
# NOTE(vponomaryov): usage of 'export_location' and
|
# NOTE(vponomaryov): usage of 'export_location' and
|
||||||
# 'export_locations' columns may cause scaling issue using API 2.9+ and
|
# 'export_locations' columns may cause scaling issue using API 2.9+ and
|
||||||
# when lots of shares are returned.
|
# when lots of shares are returned.
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
The --metadata command should handle empty metadata in proper way.
|
||||||
|
Introduced "None" metadata tag to filter list which have empty.
|
||||||
|
Example: "list --metadata None" will return only elements with metadata={}
|
Loading…
Reference in New Issue
Block a user