Skip to content

Commit 67bec77

Browse files
committed
volume: Allow filtering volume types by properties
Starting in volume API microversion 3.52, it is possible to filter volume types by extra specs (a.k.a. properties). Even non-admins can do this for so-called "user visible" extra specs. Make it possible to do this. While we're here, the test file is renamed to match the name of the module under test. Change-Id: Ia9acc06c0c615bf24e12b63146ea97ac2505f596 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
1 parent 0fb1cae commit 67bec77

3 files changed

Lines changed: 116 additions & 14 deletions

File tree

openstackclient/tests/unit/volume/v2/test_volume_type.py

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from unittest import mock
1616
from unittest.mock import call
1717

18+
from cinderclient import api_versions
1819
from osc_lib.cli import format_columns
1920
from osc_lib import exceptions
2021
from osc_lib import utils
@@ -329,7 +330,9 @@ def test_type_list_without_options(self):
329330
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
330331

331332
columns, data = self.cmd.take_action(parsed_args)
332-
self.volume_types_mock.list.assert_called_once_with(is_public=None)
333+
self.volume_types_mock.list.assert_called_once_with(
334+
search_opts={}, is_public=None
335+
)
333336
self.assertEqual(self.columns, columns)
334337
self.assertCountEqual(self.data, list(data))
335338

@@ -346,7 +349,9 @@ def test_type_list_with_options(self):
346349
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
347350

348351
columns, data = self.cmd.take_action(parsed_args)
349-
self.volume_types_mock.list.assert_called_once_with(is_public=True)
352+
self.volume_types_mock.list.assert_called_once_with(
353+
search_opts={}, is_public=True
354+
)
350355
self.assertEqual(self.columns_long, columns)
351356
self.assertCountEqual(self.data_long, list(data))
352357

@@ -362,7 +367,9 @@ def test_type_list_with_private_option(self):
362367
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
363368

364369
columns, data = self.cmd.take_action(parsed_args)
365-
self.volume_types_mock.list.assert_called_once_with(is_public=False)
370+
self.volume_types_mock.list.assert_called_once_with(
371+
search_opts={}, is_public=False
372+
)
366373
self.assertEqual(self.columns, columns)
367374
self.assertCountEqual(self.data, list(data))
368375

@@ -383,6 +390,60 @@ def test_type_list_with_default_option(self):
383390
self.assertEqual(self.columns, columns)
384391
self.assertCountEqual(self.data_with_default_type, list(data))
385392

393+
def test_type_list_with_property_option(self):
394+
self.app.client_manager.volume.api_version = api_versions.APIVersion(
395+
'3.52'
396+
)
397+
398+
arglist = [
399+
"--property",
400+
"multiattach=<is> True",
401+
]
402+
verifylist = [
403+
("encryption_type", False),
404+
("long", False),
405+
("is_public", None),
406+
("default", False),
407+
("properties", {"multiattach": "<is> True"}),
408+
]
409+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
410+
411+
columns, data = self.cmd.take_action(parsed_args)
412+
self.volume_types_mock.list.assert_called_once_with(
413+
search_opts={"extra_specs": {"multiattach": "<is> True"}},
414+
is_public=None,
415+
)
416+
self.assertEqual(self.columns, columns)
417+
self.assertCountEqual(self.data, list(data))
418+
419+
def test_type_list_with_property_option_pre_v352(self):
420+
self.app.client_manager.volume.api_version = api_versions.APIVersion(
421+
'3.51'
422+
)
423+
424+
arglist = [
425+
"--property",
426+
"multiattach=<is> True",
427+
]
428+
verifylist = [
429+
("encryption_type", False),
430+
("long", False),
431+
("is_public", None),
432+
("default", False),
433+
("properties", {"multiattach": "<is> True"}),
434+
]
435+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
436+
437+
exc = self.assertRaises(
438+
exceptions.CommandError,
439+
self.cmd.take_action,
440+
parsed_args,
441+
)
442+
self.assertIn(
443+
'--os-volume-api-version 3.52 or greater is required',
444+
str(exc),
445+
)
446+
386447
def test_type_list_with_encryption(self):
387448
encryption_type = volume_fakes.create_one_encryption_volume_type(
388449
attrs={'volume_type_id': self.volume_types[0].id},
@@ -428,7 +489,9 @@ def test_type_list_with_encryption(self):
428489

429490
columns, data = self.cmd.take_action(parsed_args)
430491
self.volume_encryption_types_mock.list.assert_called_once_with()
431-
self.volume_types_mock.list.assert_called_once_with(is_public=None)
492+
self.volume_types_mock.list.assert_called_once_with(
493+
search_opts={}, is_public=None
494+
)
432495
self.assertEqual(encryption_columns, columns)
433496
self.assertCountEqual(encryption_data, list(data))
434497

@@ -461,7 +524,7 @@ def test_type_set(self):
461524
verifylist = [
462525
('name', 'new_name'),
463526
('description', 'new_description'),
464-
('property', None),
527+
('properties', None),
465528
('volume_type', self.volume_type.id),
466529
]
467530
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -491,7 +554,7 @@ def test_type_set_property(self):
491554
verifylist = [
492555
('name', None),
493556
('description', None),
494-
('property', {'myprop': 'myvalue'}),
557+
('properties', {'myprop': 'myvalue'}),
495558
('volume_type', self.volume_type.id),
496559
]
497560
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -859,7 +922,7 @@ def test_type_unset(self):
859922
self.volume_type.id,
860923
]
861924
verifylist = [
862-
('property', ['property', 'multi_property']),
925+
('properties', ['property', 'multi_property']),
863926
('volume_type', self.volume_type.id),
864927
]
865928

openstackclient/volume/v2/volume_type.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import functools
1818
import logging
1919

20+
from cinderclient import api_versions
2021
from cliff import columns as cliff_columns
2122
from osc_lib.cli import format_columns
2223
from osc_lib.cli import parseractions
@@ -139,6 +140,7 @@ def get_parser(self, prog_name):
139140
'--property',
140141
metavar='<key=value>',
141142
action=parseractions.KeyValueAction,
143+
dest='properties',
142144
help=_(
143145
'Set a property on this volume type '
144146
'(repeat option to set multiple properties)'
@@ -232,11 +234,13 @@ def take_action(self, parsed_args):
232234
"type: %(e)s"
233235
)
234236
LOG.error(msg % {'project': parsed_args.project, 'e': e})
235-
if parsed_args.property:
236-
result = volume_type.set_keys(parsed_args.property)
237+
238+
if parsed_args.properties:
239+
result = volume_type.set_keys(parsed_args.properties)
237240
volume_type._info.update(
238241
{'properties': format_columns.DictColumn(result)}
239242
)
243+
240244
if (
241245
parsed_args.encryption_provider
242246
or parsed_args.encryption_cipher
@@ -261,6 +265,7 @@ def take_action(self, parsed_args):
261265
volume_type._info.update(
262266
{'encryption': format_columns.DictColumn(encryption._info)}
263267
)
268+
264269
volume_type._info.pop("os-volume-type-access:is_public", None)
265270

266271
return zip(*sorted(volume_type._info.items()))
@@ -348,6 +353,18 @@ def get_parser(self, prog_name):
348353
"(admin only)"
349354
),
350355
)
356+
parser.add_argument(
357+
'--property',
358+
metavar='<key=value>',
359+
action=parseractions.KeyValueAction,
360+
dest='properties',
361+
help=_(
362+
'Filter by a property on the volume types '
363+
'(repeat option to filter by multiple properties) '
364+
'(admin only except for user-visible extra specs) '
365+
'(supported by --os-volume-api-version 3.52 or above)'
366+
),
367+
)
351368
return parser
352369

353370
def take_action(self, parsed_args):
@@ -375,8 +392,22 @@ def take_action(self, parsed_args):
375392
if parsed_args.default:
376393
data = [volume_client.volume_types.default()]
377394
else:
395+
search_opts = {}
396+
397+
if parsed_args.properties:
398+
if volume_client.api_version < api_versions.APIVersion('3.52'):
399+
msg = _(
400+
"--os-volume-api-version 3.52 or greater is required "
401+
"to use the '--property' option"
402+
)
403+
raise exceptions.CommandError(msg)
404+
405+
# we pass this through as-is
406+
search_opts['extra_specs'] = parsed_args.properties
407+
378408
data = volume_client.volume_types.list(
379-
is_public=parsed_args.is_public
409+
search_opts=search_opts,
410+
is_public=parsed_args.is_public,
380411
)
381412

382413
formatters = {'Extra Specs': format_columns.DictColumn}
@@ -445,6 +476,7 @@ def get_parser(self, prog_name):
445476
'--property',
446477
metavar='<key=value>',
447478
action=parseractions.KeyValueAction,
479+
dest='properties',
448480
help=_(
449481
'Set a property on this volume type '
450482
'(repeat option to set multiple properties)'
@@ -555,9 +587,9 @@ def take_action(self, parsed_args):
555587
)
556588
result += 1
557589

558-
if parsed_args.property:
590+
if parsed_args.properties:
559591
try:
560-
volume_type.set_keys(parsed_args.property)
592+
volume_type.set_keys(parsed_args.properties)
561593
except Exception as e:
562594
LOG.error(_("Failed to set volume type property: %s"), e)
563595
result += 1
@@ -689,6 +721,7 @@ def get_parser(self, prog_name):
689721
'--property',
690722
metavar='<key>',
691723
action='append',
724+
dest='properties',
692725
help=_(
693726
'Remove a property from this volume type '
694727
'(repeat option to remove multiple properties)'
@@ -723,9 +756,9 @@ def take_action(self, parsed_args):
723756
)
724757

725758
result = 0
726-
if parsed_args.property:
759+
if parsed_args.properties:
727760
try:
728-
volume_type.unset_keys(parsed_args.property)
761+
volume_type.unset_keys(parsed_args.properties)
729762
except Exception as e:
730763
LOG.error(_("Failed to unset volume type property: %s"), e)
731764
result += 1
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
The ``volume type list`` command now accepts a ``--property <key>=<value>``
5+
option, allowing users to filter volume types by their extra spec
6+
properties.

0 commit comments

Comments
 (0)