Skip to content

Commit a6affea

Browse files
committed
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 8. get-pools Change-Id: I062e6227342ea0d940a8333e84014969c33b49df Partial: blueprint generalized-filtering-for-cinder-list-resource Depends-On: 04bd22c Depends-On: 7fdc4688fea373afb85d929e649d311568d1855a
1 parent 8cd1470 commit a6affea

13 files changed

Lines changed: 512 additions & 25 deletions

cinderclient/api_versions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
# key is a deprecated version and value is an alternative version.
3030
DEPRECATED_VERSIONS = {"1": "2"}
3131
DEPRECATED_VERSION = "2.0"
32-
MAX_VERSION = "3.28"
32+
MAX_VERSION = "3.33"
3333
MIN_VERSION = "3.0"
3434

3535
_SUBSTITUTIONS = {}

cinderclient/shell_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,26 @@ def translate_availability_zone_keys(collection):
143143
translate_keys(collection, convert)
144144

145145

146+
def extract_filters(args):
147+
filters = {}
148+
for f in args:
149+
if '=' in f:
150+
(key, value) = f.split('=', 1)
151+
if value.startswith('{') and value.endswith('}'):
152+
value = _build_internal_dict(value[1:-1])
153+
filters[key] = value
154+
155+
return filters
156+
157+
158+
def _build_internal_dict(content):
159+
result = {}
160+
for pair in content.split(','):
161+
k, v = pair.split(':', 1)
162+
result.update({k.strip(): v.strip()})
163+
return result
164+
165+
146166
def extract_metadata(args, type='user_metadata'):
147167
metadata = {}
148168
if type == 'image_metadata':
@@ -169,6 +189,11 @@ def print_group_type_list(gtypes):
169189
utils.print_list(gtypes, ['ID', 'Name', 'Description'])
170190

171191

192+
def print_resource_filter_list(filters):
193+
formatter = {'Filters': lambda resource: ', '.join(resource.filters)}
194+
utils.print_list(filters, ['Resource', 'Filters'], formatters=formatter)
195+
196+
172197
def quota_show(quotas):
173198
quotas_info_dict = utils.unicode_key_value_to_string(quotas._info)
174199
quota_dict = {}

cinderclient/tests/unit/test_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# limitations under the License.
1313

1414
import collections
15+
import ddt
1516
import sys
1617

1718
import mock
@@ -21,6 +22,7 @@
2122
from cinderclient import api_versions
2223
from cinderclient.apiclient import base as common_base
2324
from cinderclient import exceptions
25+
from cinderclient import shell_utils
2426
from cinderclient import utils
2527
from cinderclient import base
2628
from cinderclient.tests.unit import utils as test_utils
@@ -187,6 +189,21 @@ def test_build_param_with_none(self):
187189
self.assertFalse(result_2)
188190

189191

192+
@ddt.ddt
193+
class ExtractFilterTestCase(test_utils.TestCase):
194+
195+
@ddt.data({'content': ['key1=value1'],
196+
'expected': {'key1': 'value1'}},
197+
{'content': ['key1={key2:value2}'],
198+
'expected': {'key1': {'key2': 'value2'}}},
199+
{'content': ['key1=value1', 'key2={key22:value22}'],
200+
'expected': {'key1': 'value1', 'key2': {'key22': 'value22'}}})
201+
@ddt.unpack
202+
def test_extract_filters(self, content, expected):
203+
result = shell_utils.extract_filters(content)
204+
self.assertEqual(expected, result)
205+
206+
190207
class PrintListTestCase(test_utils.TestCase):
191208

192209
def test_print_list_with_list(self):

cinderclient/tests/unit/v3/fakes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,12 @@ def get_messages_12345(self, **kw):
544544
}
545545
return 200, {}, {'message': message}
546546

547+
#
548+
# resource filters
549+
#
550+
def get_resource_filters(self, **kw):
551+
return 200, {}, {'resource_filters': []}
552+
547553

548554
def fake_request_get():
549555
versions = {'versions': [{'id': 'v1.0',
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import ddt
14+
15+
from cinderclient.tests.unit import utils
16+
from cinderclient.tests.unit.v3 import fakes
17+
18+
cs = fakes.FakeClient()
19+
20+
21+
@ddt.ddt
22+
class ResourceFilterTests(utils.TestCase):
23+
@ddt.data({'resource': None, 'query_url': None},
24+
{'resource': 'volume', 'query_url': '?resource=volume'},
25+
{'resource': 'group', 'query_url': '?resource=group'})
26+
@ddt.unpack
27+
def test_list_messages(self, resource, query_url):
28+
cs.resource_filters.list(resource)
29+
url = '/resource_filters'
30+
if resource is not None:
31+
url += query_url
32+
cs.assert_called('GET', url)

cinderclient/tests/unit/v3/test_shell.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,104 @@ def assert_called(self, method, url, body=None,
6666
return self.shell.cs.assert_called(method, url, body,
6767
partial_body, **kwargs)
6868

69+
@ddt.data({'resource': None, 'query_url': None},
70+
{'resource': 'volume', 'query_url': '?resource=volume'},
71+
{'resource': 'group', 'query_url': '?resource=group'})
72+
@ddt.unpack
73+
def test_list_filters(self, resource, query_url):
74+
url = '/resource_filters'
75+
if resource is not None:
76+
url += query_url
77+
self.run_command('--os-volume-api-version 3.33 '
78+
'list-filters --resource=%s' % resource)
79+
else:
80+
self.run_command('--os-volume-api-version 3.33 list-filters')
81+
82+
self.assert_called('GET', url)
83+
84+
@ddt.data(
85+
# testcases for list volume
86+
{'command':
87+
'list --name=123 --filters name=456',
88+
'expected':
89+
'/volumes/detail?name=456'},
90+
{'command':
91+
'list --filters name=123',
92+
'expected':
93+
'/volumes/detail?name=123'},
94+
{'command':
95+
'list --filters metadata={key1:value1}',
96+
'expected':
97+
'/volumes/detail?metadata=%7B%27key1%27%3A+%27value1%27%7D'},
98+
# testcases for list group
99+
{'command':
100+
'group-list --filters name=456',
101+
'expected':
102+
'/groups/detail?name=456'},
103+
{'command':
104+
'group-list --filters status=available',
105+
'expected':
106+
'/groups/detail?status=available'},
107+
# testcases for list group-snapshot
108+
{'command':
109+
'group-snapshot-list --status=error --filters status=available',
110+
'expected':
111+
'/group_snapshots/detail?status=available'},
112+
{'command':
113+
'group-snapshot-list --filters availability_zone=123',
114+
'expected':
115+
'/group_snapshots/detail?availability_zone=123'},
116+
# testcases for list message
117+
{'command':
118+
'message-list --event_id=123 --filters event_id=456',
119+
'expected':
120+
'/messages?event_id=456'},
121+
{'command':
122+
'message-list --filters request_id=123',
123+
'expected':
124+
'/messages?request_id=123'},
125+
# testcases for list attachment
126+
{'command':
127+
'attachment-list --volume-id=123 --filters volume_id=456',
128+
'expected':
129+
'/attachments?volume_id=456'},
130+
{'command':
131+
'attachment-list --filters mountpoint=123',
132+
'expected':
133+
'/attachments?mountpoint=123'},
134+
# testcases for list backup
135+
{'command':
136+
'backup-list --volume-id=123 --filters volume_id=456',
137+
'expected':
138+
'/backups/detail?volume_id=456'},
139+
{'command':
140+
'backup-list --filters name=123',
141+
'expected':
142+
'/backups/detail?name=123'},
143+
# testcases for list snapshot
144+
{'command':
145+
'snapshot-list --volume-id=123 --filters volume_id=456',
146+
'expected':
147+
'/snapshots/detail?volume_id=456'},
148+
{'command':
149+
'snapshot-list --filters name=123',
150+
'expected':
151+
'/snapshots/detail?name=123'},
152+
# testcases for get pools
153+
{'command':
154+
'get-pools --filters name=456 --detail',
155+
'expected':
156+
'/scheduler-stats/get_pools?detail=True&name=456'},
157+
{'command':
158+
'get-pools --filters name=456',
159+
'expected':
160+
'/scheduler-stats/get_pools?name=456'}
161+
)
162+
@ddt.unpack
163+
def test_list_with_filters_mixed(self, command, expected):
164+
self.run_command('--os-volume-api-version 3.33 %s' % command)
165+
self.assert_called('GET', expected)
166+
69167
def test_list(self):
70168
self.run_command('list')
71169
# NOTE(jdg): we default to detail currently

cinderclient/tests/unit/v3/test_volumes.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# License for the specific language governing permissions and limitations
1616
# under the License.
1717

18+
import ddt
19+
1820
from cinderclient import api_versions
1921
from cinderclient.tests.unit import utils
2022
from cinderclient.tests.unit.v3 import fakes
@@ -25,6 +27,7 @@
2527
cs = fakes.FakeClient()
2628

2729

30+
@ddt.data
2831
class VolumesTest(utils.TestCase):
2932

3033
def test_volume_manager_upload_to_image(self):
@@ -100,3 +103,13 @@ def test_list_with_image_metadata(self):
100103
expected = ("/volumes/detail?glance_metadata=%s"
101104
% parse.quote_plus("{'key1': 'val1'}"))
102105
cs.assert_called('GET', expected)
106+
107+
@ddt.data(True, False)
108+
def test_get_pools_filter_by_name(self, detail):
109+
cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.33'))
110+
vol = cs.volumes.get_pools(detail, 'pool1')
111+
request_url = '/scheduler-stats/get_pools?name=pool1'
112+
if detail:
113+
request_url = '/scheduler-stats/get_pools?detail=True&name=pool1'
114+
cs.assert_called('GET', request_url)
115+
self._assert_request_id(vol)

cinderclient/v3/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from cinderclient.v3 import qos_specs
3333
from cinderclient.v3 import quota_classes
3434
from cinderclient.v3 import quotas
35+
from cinderclient.v3 import resource_filters
3536
from cinderclient.v3 import services
3637
from cinderclient.v3 import volumes
3738
from cinderclient.v3 import volume_snapshots
@@ -85,6 +86,7 @@ def __init__(self, username=None, api_key=None, project_id=None,
8586
self.quotas = quotas.QuotaSetManager(self)
8687
self.backups = volume_backups.VolumeBackupManager(self)
8788
self.messages = messages.MessageManager(self)
89+
self.resource_filters = resource_filters.ResourceFilterManager(self)
8890
self.restores = volume_backups_restore.VolumeBackupRestoreManager(self)
8991
self.transfers = volume_transfers.VolumeTransferManager(self)
9092
self.services = services.ServiceManager(self)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
"""Resource filters interface."""
14+
15+
from cinderclient import base
16+
from cinderclient import api_versions
17+
18+
19+
class ResourceFilter(base.Resource):
20+
NAME_ATTR = 'resource'
21+
22+
def __repr__(self):
23+
return "<ResourceFilter: %s>" % self.resource
24+
25+
26+
class ResourceFilterManager(base.ManagerWithFind):
27+
"""Manage :class:`ResourceFilter` resources."""
28+
29+
resource_class = ResourceFilter
30+
31+
@api_versions.wraps('3.33')
32+
def list(self, resource=None):
33+
"""List all resource filters."""
34+
url = '/resource_filters'
35+
if resource is not None:
36+
url += '?resource=%s' % resource
37+
return self._list(url, "resource_filters")

0 commit comments

Comments
 (0)