Skip to content

Commit a4394eb

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Add filter to image list"
2 parents c3aad41 + 61a4034 commit a4394eb

11 files changed

Lines changed: 606 additions & 130 deletions

File tree

doc/source/command-objects/image.rst

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,10 @@ List available images
138138
.. code:: bash
139139
140140
os image list
141-
[--page-size <size>]
142-
[--public|--private]
141+
[--public | --private | --shared]
142+
[--property <key=value>]
143143
[--long]
144144
145-
.. option:: --page-size <size>
146-
147-
Number of images to request in each paginated request
148-
149145
.. option:: --public
150146

151147
List only public images
@@ -154,6 +150,16 @@ List available images
154150

155151
List only private images
156152

153+
.. option:: --shared
154+
155+
List only shared images
156+
157+
*Image version 2 only.*
158+
159+
.. option:: --property <key=value>
160+
161+
Filter output based on property
162+
157163
.. option:: --long
158164

159165
List additional fields in output

openstackclient/api/image_v1.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ def image_list(
4949
http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-a-list-of-public-vm-images.html
5050
http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-detailed-metadata-on-public-vm-images.html
5151
http://docs.openstack.org/api/openstack-image-service/1.1/content/filtering-images-returned-via-get-images-and-get-imagesdetail.html
52-
53-
TODO(dtroyer): Implement filtering
5452
"""
5553

5654
url = "/images"

openstackclient/api/image_v2.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def image_list(
3030
detailed=False,
3131
public=False,
3232
private=False,
33+
shared=False,
3334
**filter
3435
):
3536
"""Get available images
@@ -49,17 +50,17 @@ def image_list(
4950
both public and private images which is the same set as all images.
5051
5152
http://docs.openstack.org/api/openstack-image-service/2.0/content/list-images.html
52-
53-
TODO(dtroyer): Implement filtering
5453
"""
5554

56-
if public == private:
57-
# No filtering for both False and both True cases
55+
if not public and not private and not shared:
56+
# No filtering for all False
5857
filter.pop('visibility', None)
5958
elif public:
6059
filter['visibility'] = 'public'
6160
elif private:
6261
filter['visibility'] = 'private'
62+
elif shared:
63+
filter['visibility'] = 'shared'
6364

6465
url = "/images"
6566
if detailed:

openstackclient/api/utils.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
14+
"""API Utilities Library"""
15+
16+
17+
def simple_filter(
18+
data=None,
19+
attr=None,
20+
value=None,
21+
property_field=None,
22+
):
23+
"""Filter a list of dicts
24+
25+
:param list data:
26+
The list to be filtered. The list is modified in-place and will
27+
be changed if any filtering occurs.
28+
:param string attr:
29+
The name of the attribute to filter. If attr does not exist no
30+
match will succeed and no rows will be retrurned. If attr is
31+
None no filtering will be performed and all rows will be returned.
32+
:param sring value:
33+
The value to filter. None is considered to be a 'no filter' value.
34+
'' matches agains a Python empty string.
35+
:param string property_field:
36+
The name of the data field containing a property dict to filter.
37+
If property_field is None, attr is a field name. If property_field
38+
is not None, attr is a property key name inside the named property
39+
field.
40+
41+
:returns:
42+
Returns the filtered list
43+
:rtype list:
44+
45+
This simple filter (one attribute, one exact-match value) searches a
46+
list of dicts to select items. It first searches the item dict for a
47+
matching ``attr`` then does an exact-match on the ``value``. If
48+
``property_field`` is given, it will look inside that field (if it
49+
exists and is a dict) for a matching ``value``.
50+
"""
51+
52+
# Take the do-nothing case shortcut
53+
if not data or not attr or value is None:
54+
return data
55+
56+
# NOTE:(dtroyer): This filter modifies the provided list in-place using
57+
# list.remove() so we need to start at the end so the loop pointer does
58+
# not skip any items after a deletion.
59+
for d in reversed(data):
60+
if attr in d:
61+
# Searching data fields
62+
search_value = d[attr]
63+
elif (property_field and property_field in d and
64+
type(d[property_field]) is dict):
65+
# Searching a properties field - do this separately because
66+
# we don't want to fail over to checking the fields if a
67+
# property name is given.
68+
if attr in d[property_field]:
69+
search_value = d[property_field][attr]
70+
else:
71+
search_value = None
72+
else:
73+
search_value = None
74+
75+
# could do regex here someday...
76+
if not search_value or search_value != value:
77+
# remove from list
78+
try:
79+
data.remove(d)
80+
except ValueError:
81+
# it's already gone!
82+
pass
83+
84+
return data

openstackclient/image/v1/image.py

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
"""Image V1 Action Implementations"""
1717

18+
import argparse
1819
import io
1920
import logging
2021
import os
@@ -31,6 +32,7 @@
3132
from cliff import show
3233

3334
from glanceclient.common import utils as gc_utils
35+
from openstackclient.api import utils as api_utils
3436
from openstackclient.common import exceptions
3537
from openstackclient.common import parseractions
3638
from openstackclient.common import utils
@@ -40,6 +42,21 @@
4042
DEFAULT_DISK_FORMAT = 'raw'
4143

4244

45+
def _format_visibility(data):
46+
"""Return a formatted visibility string
47+
48+
:param data:
49+
The server's visibility (is_public) status value: True, False
50+
:rtype:
51+
A string formatted to public/private
52+
"""
53+
54+
if data:
55+
return 'public'
56+
else:
57+
return 'private'
58+
59+
4360
class CreateImage(show.ShowOne):
4461
"""Create/upload an image"""
4562

@@ -295,11 +312,6 @@ class ListImage(lister.Lister):
295312

296313
def get_parser(self, prog_name):
297314
parser = super(ListImage, self).get_parser(prog_name)
298-
parser.add_argument(
299-
"--page-size",
300-
metavar="<size>",
301-
help="Number of images to request in each paginated request",
302-
)
303315
public_group = parser.add_mutually_exclusive_group()
304316
public_group.add_argument(
305317
"--public",
@@ -315,12 +327,34 @@ def get_parser(self, prog_name):
315327
default=False,
316328
help="List only private images",
317329
)
330+
# Included for silent CLI compatibility with v2
331+
public_group.add_argument(
332+
"--shared",
333+
dest="shared",
334+
action="store_true",
335+
default=False,
336+
help=argparse.SUPPRESS,
337+
)
338+
parser.add_argument(
339+
'--property',
340+
metavar='<key=value>',
341+
action=parseractions.KeyValueAction,
342+
help='Filter output based on property',
343+
)
318344
parser.add_argument(
319345
'--long',
320346
action='store_true',
321347
default=False,
322348
help='List additional fields in output',
323349
)
350+
351+
# --page-size has never worked, leave here for silent compatability
352+
# We'll implement limit/marker differently later
353+
parser.add_argument(
354+
"--page-size",
355+
metavar="<size>",
356+
help=argparse.SUPPRESS,
357+
)
324358
return parser
325359

326360
def take_action(self, parsed_args):
@@ -329,23 +363,63 @@ def take_action(self, parsed_args):
329363
image_client = self.app.client_manager.image
330364

331365
kwargs = {}
332-
if parsed_args.page_size is not None:
333-
kwargs["page_size"] = parsed_args.page_size
334366
if parsed_args.public:
335367
kwargs['public'] = True
336368
if parsed_args.private:
337369
kwargs['private'] = True
338-
kwargs['detailed'] = parsed_args.long
370+
kwargs['detailed'] = bool(parsed_args.property or parsed_args.long)
339371

340372
if parsed_args.long:
341-
columns = ('ID', 'Name', 'Disk Format', 'Container Format',
342-
'Size', 'Status')
373+
columns = (
374+
'ID',
375+
'Name',
376+
'Disk Format',
377+
'Container Format',
378+
'Size',
379+
'Status',
380+
'is_public',
381+
'protected',
382+
'owner',
383+
'properties',
384+
)
385+
column_headers = (
386+
'ID',
387+
'Name',
388+
'Disk Format',
389+
'Container Format',
390+
'Size',
391+
'Status',
392+
'Visibility',
393+
'Protected',
394+
'Owner',
395+
'Properties',
396+
)
343397
else:
344398
columns = ("ID", "Name")
399+
column_headers = columns
345400

346401
data = image_client.api.image_list(**kwargs)
347402

348-
return (columns, (utils.get_dict_properties(s, columns) for s in data))
403+
if parsed_args.property:
404+
# NOTE(dtroyer): coerce to a list to subscript it in py3
405+
attr, value = list(parsed_args.property.items())[0]
406+
api_utils.simple_filter(
407+
data,
408+
attr=attr,
409+
value=value,
410+
property_field='properties',
411+
)
412+
return (
413+
column_headers,
414+
(utils.get_dict_properties(
415+
s,
416+
columns,
417+
formatters={
418+
'is_public': _format_visibility,
419+
'properties': utils.format_dict,
420+
},
421+
) for s in data)
422+
)
349423

350424

351425
class SaveImage(command.Command):

0 commit comments

Comments
 (0)