Add filter to image list

* Hides previously broken --page-size option
* Adds --property to image list for filtering on properties
* Adds Visibility, Protected, Owner, Properties/Tags to --long output
* Adds api.utils.simple_filter() for selecting matches out of a list
  of objects
* Adds tests for all of the above
* Updates image docs

There are additional filtering options to be added in later reviews.

Change-Id: I32feff0ad61aae749b33621c817658d7dc90c3aa
Closes-bug: 1401902
This commit is contained in:
Dean Troyer 2015-01-21 15:02:58 -06:00
parent 2c03f6f42f
commit 61a40343fd
11 changed files with 606 additions and 130 deletions

View File

@ -138,14 +138,10 @@ List available images
.. code:: bash
os image list
[--page-size <size>]
[--public|--private]
[--public | --private | --shared]
[--property <key=value>]
[--long]
.. option:: --page-size <size>
Number of images to request in each paginated request
.. option:: --public
List only public images
@ -154,6 +150,16 @@ List available images
List only private images
.. option:: --shared
List only shared images
*Image version 2 only.*
.. option:: --property <key=value>
Filter output based on property
.. option:: --long
List additional fields in output

View File

@ -49,8 +49,6 @@ class APIv1(api.BaseAPI):
http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-a-list-of-public-vm-images.html
http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-detailed-metadata-on-public-vm-images.html
http://docs.openstack.org/api/openstack-image-service/1.1/content/filtering-images-returned-via-get-images-and-get-imagesdetail.html
TODO(dtroyer): Implement filtering
"""
url = "/images"

View File

@ -30,6 +30,7 @@ class APIv2(image_v1.APIv1):
detailed=False,
public=False,
private=False,
shared=False,
**filter
):
"""Get available images
@ -49,17 +50,17 @@ class APIv2(image_v1.APIv1):
both public and private images which is the same set as all images.
http://docs.openstack.org/api/openstack-image-service/2.0/content/list-images.html
TODO(dtroyer): Implement filtering
"""
if public == private:
# No filtering for both False and both True cases
if not public and not private and not shared:
# No filtering for all False
filter.pop('visibility', None)
elif public:
filter['visibility'] = 'public'
elif private:
filter['visibility'] = 'private'
elif shared:
filter['visibility'] = 'shared'
url = "/images"
if detailed:

View File

@ -0,0 +1,84 @@
# 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.
#
"""API Utilities Library"""
def simple_filter(
data=None,
attr=None,
value=None,
property_field=None,
):
"""Filter a list of dicts
:param list data:
The list to be filtered. The list is modified in-place and will
be changed if any filtering occurs.
:param string attr:
The name of the attribute to filter. If attr does not exist no
match will succeed and no rows will be retrurned. If attr is
None no filtering will be performed and all rows will be returned.
:param sring value:
The value to filter. None is considered to be a 'no filter' value.
'' matches agains a Python empty string.
:param string property_field:
The name of the data field containing a property dict to filter.
If property_field is None, attr is a field name. If property_field
is not None, attr is a property key name inside the named property
field.
:returns:
Returns the filtered list
:rtype list:
This simple filter (one attribute, one exact-match value) searches a
list of dicts to select items. It first searches the item dict for a
matching ``attr`` then does an exact-match on the ``value``. If
``property_field`` is given, it will look inside that field (if it
exists and is a dict) for a matching ``value``.
"""
# Take the do-nothing case shortcut
if not data or not attr or value is None:
return data
# NOTE:(dtroyer): This filter modifies the provided list in-place using
# list.remove() so we need to start at the end so the loop pointer does
# not skip any items after a deletion.
for d in reversed(data):
if attr in d:
# Searching data fields
search_value = d[attr]
elif (property_field and property_field in d and
type(d[property_field]) is dict):
# Searching a properties field - do this separately because
# we don't want to fail over to checking the fields if a
# property name is given.
if attr in d[property_field]:
search_value = d[property_field][attr]
else:
search_value = None
else:
search_value = None
# could do regex here someday...
if not search_value or search_value != value:
# remove from list
try:
data.remove(d)
except ValueError:
# it's already gone!
pass
return data

View File

@ -15,6 +15,7 @@
"""Image V1 Action Implementations"""
import argparse
import io
import logging
import os
@ -31,6 +32,7 @@ from cliff import lister
from cliff import show
from glanceclient.common import utils as gc_utils
from openstackclient.api import utils as api_utils
from openstackclient.common import exceptions
from openstackclient.common import parseractions
from openstackclient.common import utils
@ -40,6 +42,21 @@ DEFAULT_CONTAINER_FORMAT = 'bare'
DEFAULT_DISK_FORMAT = 'raw'
def _format_visibility(data):
"""Return a formatted visibility string
:param data:
The server's visibility (is_public) status value: True, False
:rtype:
A string formatted to public/private
"""
if data:
return 'public'
else:
return 'private'
class CreateImage(show.ShowOne):
"""Create/upload an image"""
@ -295,11 +312,6 @@ class ListImage(lister.Lister):
def get_parser(self, prog_name):
parser = super(ListImage, self).get_parser(prog_name)
parser.add_argument(
"--page-size",
metavar="<size>",
help="Number of images to request in each paginated request",
)
public_group = parser.add_mutually_exclusive_group()
public_group.add_argument(
"--public",
@ -315,12 +327,34 @@ class ListImage(lister.Lister):
default=False,
help="List only private images",
)
# Included for silent CLI compatibility with v2
public_group.add_argument(
"--shared",
dest="shared",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
parser.add_argument(
'--property',
metavar='<key=value>',
action=parseractions.KeyValueAction,
help='Filter output based on property',
)
parser.add_argument(
'--long',
action='store_true',
default=False,
help='List additional fields in output',
)
# --page-size has never worked, leave here for silent compatability
# We'll implement limit/marker differently later
parser.add_argument(
"--page-size",
metavar="<size>",
help=argparse.SUPPRESS,
)
return parser
def take_action(self, parsed_args):
@ -329,23 +363,63 @@ class ListImage(lister.Lister):
image_client = self.app.client_manager.image
kwargs = {}
if parsed_args.page_size is not None:
kwargs["page_size"] = parsed_args.page_size
if parsed_args.public:
kwargs['public'] = True
if parsed_args.private:
kwargs['private'] = True
kwargs['detailed'] = parsed_args.long
kwargs['detailed'] = bool(parsed_args.property or parsed_args.long)
if parsed_args.long:
columns = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
columns = (
'ID',
'Name',
'Disk Format',
'Container Format',
'Size',
'Status',
'is_public',
'protected',
'owner',
'properties',
)
column_headers = (
'ID',
'Name',
'Disk Format',
'Container Format',
'Size',
'Status',
'Visibility',
'Protected',
'Owner',
'Properties',
)
else:
columns = ("ID", "Name")
column_headers = columns
data = image_client.api.image_list(**kwargs)
return (columns, (utils.get_dict_properties(s, columns) for s in data))
if parsed_args.property:
# NOTE(dtroyer): coerce to a list to subscript it in py3
attr, value = list(parsed_args.property.items())[0]
api_utils.simple_filter(
data,
attr=attr,
value=value,
property_field='properties',
)
return (
column_headers,
(utils.get_dict_properties(
s,
columns,
formatters={
'is_public': _format_visibility,
'properties': utils.format_dict,
},
) for s in data)
)
class SaveImage(command.Command):

View File

@ -15,6 +15,7 @@
"""Image V2 Action Implementations"""
import argparse
import logging
import six
@ -23,6 +24,8 @@ from cliff import lister
from cliff import show
from glanceclient.common import utils as gc_utils
from openstackclient.api import utils as api_utils
from openstackclient.common import parseractions
from openstackclient.common import utils
@ -60,11 +63,6 @@ class ListImage(lister.Lister):
def get_parser(self, prog_name):
parser = super(ListImage, self).get_parser(prog_name)
parser.add_argument(
"--page-size",
metavar="<size>",
help="Number of images to request in each paginated request",
)
public_group = parser.add_mutually_exclusive_group()
public_group.add_argument(
"--public",
@ -80,12 +78,33 @@ class ListImage(lister.Lister):
default=False,
help="List only private images",
)
public_group.add_argument(
"--shared",
dest="shared",
action="store_true",
default=False,
help="List only shared images",
)
parser.add_argument(
'--property',
metavar='<key=value>',
action=parseractions.KeyValueAction,
help='Filter output based on property',
)
parser.add_argument(
'--long',
action='store_true',
default=False,
help='List additional fields in output',
)
# --page-size has never worked, leave here for silent compatability
# We'll implement limit/marker differently later
parser.add_argument(
"--page-size",
metavar="<size>",
help=argparse.SUPPRESS,
)
return parser
def take_action(self, parsed_args):
@ -94,23 +113,63 @@ class ListImage(lister.Lister):
image_client = self.app.client_manager.image
kwargs = {}
if parsed_args.page_size is not None:
kwargs["page_size"] = parsed_args.page_size
if parsed_args.public:
kwargs['public'] = True
if parsed_args.private:
kwargs['private'] = True
kwargs['detailed'] = parsed_args.long
if parsed_args.shared:
kwargs['shared'] = True
if parsed_args.long:
columns = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
columns = (
'ID',
'Name',
'Disk Format',
'Container Format',
'Size',
'Status',
'visibility',
'protected',
'owner',
'tags',
)
column_headers = (
'ID',
'Name',
'Disk Format',
'Container Format',
'Size',
'Status',
'Visibility',
'Protected',
'Owner',
'Tags',
)
else:
columns = ("ID", "Name")
column_headers = columns
data = image_client.api.image_list(**kwargs)
return (columns, (utils.get_dict_properties(s, columns) for s in data))
if parsed_args.property:
# NOTE(dtroyer): coerce to a list to subscript it in py3
attr, value = list(parsed_args.property.items())[0]
api_utils.simple_filter(
data,
attr=attr,
value=value,
property_field='properties',
)
return (
column_headers,
(utils.get_dict_properties(
s,
columns,
formatters={
'tags': utils.format_dict,
},
) for s in data)
)
class SaveImage(command.Command):

View File

@ -0,0 +1,56 @@
# 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.
#
"""API Test Fakes"""
from requests_mock.contrib import fixture
from keystoneclient import session
from openstackclient.tests import utils
RESP_ITEM_1 = {
'id': '1',
'name': 'alpha',
'status': 'UP',
'props': {'a': 1, 'b': 2},
}
RESP_ITEM_2 = {
'id': '2',
'name': 'beta',
'status': 'DOWN',
'props': {'a': 2, 'b': 2},
}
RESP_ITEM_3 = {
'id': '3',
'name': 'delta',
'status': 'UP',
'props': {'a': 3, 'b': 1},
}
LIST_RESP = [RESP_ITEM_1, RESP_ITEM_2]
LIST_BODY = {
'p1': 'xxx',
'p2': 'yyy',
}
class TestSession(utils.TestCase):
BASE_URL = 'https://api.example.com:1234/vX'
def setUp(self):
super(TestSession, self).setUp()
self.sess = session.Session()
self.requests_mock = self.useFixture(fixture.Fixture())

View File

@ -13,49 +13,12 @@
"""Base API Library Tests"""
from requests_mock.contrib import fixture
from keystoneclient import session
from openstackclient.api import api
from openstackclient.common import exceptions
from openstackclient.tests import utils
from openstackclient.tests.api import fakes as api_fakes
RESP_ITEM_1 = {
'id': '1',
'name': 'alpha',
'status': 'UP',
}
RESP_ITEM_2 = {
'id': '2',
'name': 'beta',
'status': 'DOWN',
}
RESP_ITEM_3 = {
'id': '3',
'name': 'delta',
'status': 'UP',
}
LIST_RESP = [RESP_ITEM_1, RESP_ITEM_2]
LIST_BODY = {
'p1': 'xxx',
'p2': 'yyy',
}
class TestSession(utils.TestCase):
BASE_URL = 'https://api.example.com:1234/vX'
def setUp(self):
super(TestSession, self).setUp()
self.sess = session.Session()
self.requests_mock = self.useFixture(fixture.Fixture())
class TestKeystoneSession(TestSession):
class TestKeystoneSession(api_fakes.TestSession):
def setUp(self):
super(TestKeystoneSession, self).setUp()
@ -68,14 +31,14 @@ class TestKeystoneSession(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz',
json=RESP_ITEM_1,
json=api_fakes.RESP_ITEM_1,
status_code=200,
)
ret = self.api._request('GET', '/qaz')
self.assertEqual(RESP_ITEM_1, ret.json())
self.assertEqual(api_fakes.RESP_ITEM_1, ret.json())
class TestBaseAPI(TestSession):
class TestBaseAPI(api_fakes.TestSession):
def setUp(self):
super(TestBaseAPI, self).setUp()
@ -88,21 +51,21 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'POST',
self.BASE_URL + '/qaz',
json=RESP_ITEM_1,
json=api_fakes.RESP_ITEM_1,
status_code=202,
)
ret = self.api.create('qaz')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
def test_create_put(self):
self.requests_mock.register_uri(
'PUT',
self.BASE_URL + '/qaz',
json=RESP_ITEM_1,
json=api_fakes.RESP_ITEM_1,
status_code=202,
)
ret = self.api.create('qaz', method='PUT')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
def test_delete(self):
self.requests_mock.register_uri(
@ -127,11 +90,11 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz?id=1',
json={'qaz': [RESP_ITEM_1]},
json={'qaz': [api_fakes.RESP_ITEM_1]},
status_code=200,
)
ret = self.api.find_attr('qaz', '1')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
# value not found
self.requests_mock.register_uri(
@ -157,23 +120,23 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz?status=UP',
json={'qaz': [RESP_ITEM_1]},
json={'qaz': [api_fakes.RESP_ITEM_1]},
status_code=200,
)
ret = self.api.find_attr('qaz', 'UP', attr='status')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
ret = self.api.find_attr('qaz', value='UP', attr='status')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
def test_find_attr_by_name(self):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz?name=alpha',
json={'qaz': [RESP_ITEM_1]},
json={'qaz': [api_fakes.RESP_ITEM_1]},
status_code=200,
)
ret = self.api.find_attr('qaz', 'alpha')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
# value not found
self.requests_mock.register_uri(
@ -199,13 +162,13 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz?status=UP',
json={'qaz': [RESP_ITEM_1]},
json={'qaz': [api_fakes.RESP_ITEM_1]},
status_code=200,
)
ret = self.api.find_attr('qaz', 'UP', attr='status')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
ret = self.api.find_attr('qaz', value='UP', attr='status')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
def test_find_attr_path_resource(self):
@ -219,37 +182,37 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/wsx?id=1',
json={'qaz': [RESP_ITEM_1]},
json={'qaz': [api_fakes.RESP_ITEM_1]},
status_code=200,
)
ret = self.api.find_attr('wsx', '1', resource='qaz')
self.assertEqual(RESP_ITEM_1, ret)
self.assertEqual(api_fakes.RESP_ITEM_1, ret)
def test_find_bulk_none(self):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.find_bulk('qaz')
self.assertEqual(LIST_RESP, ret)
self.assertEqual(api_fakes.LIST_RESP, ret)
def test_find_bulk_one(self):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.find_bulk('qaz', id='1')
self.assertEqual([LIST_RESP[0]], ret)
self.assertEqual([api_fakes.LIST_RESP[0]], ret)
ret = self.api.find_bulk('qaz', id='0')
self.assertEqual([], ret)
ret = self.api.find_bulk('qaz', name='beta')
self.assertEqual([LIST_RESP[1]], ret)
self.assertEqual([api_fakes.LIST_RESP[1]], ret)
ret = self.api.find_bulk('qaz', error='bogus')
self.assertEqual([], ret)
@ -258,11 +221,11 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.find_bulk('qaz', id='1', name='alpha')
self.assertEqual([LIST_RESP[0]], ret)
self.assertEqual([api_fakes.LIST_RESP[0]], ret)
ret = self.api.find_bulk('qaz', id='1', name='beta')
self.assertEqual([], ret)
@ -274,11 +237,11 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz',
json={'qaz': LIST_RESP},
json={'qaz': api_fakes.LIST_RESP},
status_code=200,
)
ret = self.api.find_bulk('qaz', id='1')
self.assertEqual([LIST_RESP[0]], ret)
self.assertEqual([api_fakes.LIST_RESP[0]], ret)
# list tests
@ -286,77 +249,77 @@ class TestBaseAPI(TestSession):
self.requests_mock.register_uri(
'GET',
self.BASE_URL,
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.list('')
self.assertEqual(LIST_RESP, ret)
self.assertEqual(api_fakes.LIST_RESP, ret)
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.list('qaz')
self.assertEqual(LIST_RESP, ret)
self.assertEqual(api_fakes.LIST_RESP, ret)
def test_list_params(self):
params = {'format': 'json'}
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '?format=json',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.list('', **params)
self.assertEqual(LIST_RESP, ret)
self.assertEqual(api_fakes.LIST_RESP, ret)
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz?format=json',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.list('qaz', **params)
self.assertEqual(LIST_RESP, ret)
self.assertEqual(api_fakes.LIST_RESP, ret)
def test_list_body(self):
self.requests_mock.register_uri(
'POST',
self.BASE_URL + '/qaz',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.list('qaz', body=LIST_BODY)
self.assertEqual(LIST_RESP, ret)
ret = self.api.list('qaz', body=api_fakes.LIST_BODY)
self.assertEqual(api_fakes.LIST_RESP, ret)
def test_list_detailed(self):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz/details',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.list('qaz', detailed=True)
self.assertEqual(LIST_RESP, ret)
self.assertEqual(api_fakes.LIST_RESP, ret)
def test_list_filtered(self):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz?attr=value',
json=LIST_RESP,
json=api_fakes.LIST_RESP,
status_code=200,
)
ret = self.api.list('qaz', attr='value')
self.assertEqual(LIST_RESP, ret)
self.assertEqual(api_fakes.LIST_RESP, ret)
def test_list_wrapped(self):
self.requests_mock.register_uri(
'GET',
self.BASE_URL + '/qaz?attr=value',
json={'responses': LIST_RESP},
json={'responses': api_fakes.LIST_RESP},
status_code=200,
)
ret = self.api.list('qaz', attr='value')
self.assertEqual({'responses': LIST_RESP}, ret)
self.assertEqual({'responses': api_fakes.LIST_RESP}, ret)

View File

@ -0,0 +1,115 @@
# 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.
#
"""API Utilities Library Tests"""
import copy
from openstackclient.api import api
from openstackclient.api import utils as api_utils
from openstackclient.tests.api import fakes as api_fakes
class TestBaseAPIFilter(api_fakes.TestSession):
"""The filters can be tested independently"""
def setUp(self):
super(TestBaseAPIFilter, self).setUp()
self.api = api.BaseAPI(
session=self.sess,
endpoint=self.BASE_URL,
)
self.input_list = [
api_fakes.RESP_ITEM_1,
api_fakes.RESP_ITEM_2,
api_fakes.RESP_ITEM_3,
]
def test_simple_filter_none(self):
output = api_utils.simple_filter(
)
self.assertIsNone(output)
def test_simple_filter_no_attr(self):
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
)
self.assertEqual(self.input_list, output)
def test_simple_filter_attr_only(self):
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='status',
)
self.assertEqual(self.input_list, output)
def test_simple_filter_attr_value(self):
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='status',
value='',
)
self.assertEqual([], output)
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='status',
value='UP',
)
self.assertEqual(
[api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_3],
output,
)
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='fred',
value='UP',
)
self.assertEqual([], output)
def test_simple_filter_prop_attr_only(self):
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='b',
property_field='props',
)
self.assertEqual(self.input_list, output)
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='status',
property_field='props',
)
self.assertEqual(self.input_list, output)
def test_simple_filter_prop_attr_value(self):
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='b',
value=2,
property_field='props',
)
self.assertEqual(
[api_fakes.RESP_ITEM_1, api_fakes.RESP_ITEM_2],
output,
)
output = api_utils.simple_filter(
copy.deepcopy(self.input_list),
attr='b',
value=9,
property_field='props',
)
self.assertEqual([], output)

View File

@ -407,8 +407,18 @@ class TestImageList(TestImage):
detailed=True,
)
collist = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
collist = (
'ID',
'Name',
'Disk Format',
'Container Format',
'Size',
'Status',
'Visibility',
'Protected',
'Owner',
'Properties',
)
self.assertEqual(collist, columns)
datalist = ((
@ -418,6 +428,45 @@ class TestImageList(TestImage):
'',
'',
'',
'public',
False,
image_fakes.image_owner,
"Alpha='a', Beta='b', Gamma='g'",
), )
self.assertEqual(datalist, tuple(data))
@mock.patch('openstackclient.api.utils.simple_filter')
def test_image_list_property_option(self, sf_mock):
sf_mock.return_value = [
copy.deepcopy(image_fakes.IMAGE),
]
arglist = [
'--property', 'a=1',
]
verifylist = [
('property', {'a': '1'}),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=True,
)
sf_mock.assert_called_with(
[image_fakes.IMAGE],
attr='a',
value='1',
property_field='properties',
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))

View File

@ -83,15 +83,14 @@ class TestImageList(TestImage):
verifylist = [
('public', False),
('private', False),
('shared', False),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
)
self.api_mock.image_list.assert_called_with()
collist = ('ID', 'Name')
@ -109,6 +108,7 @@ class TestImageList(TestImage):
verifylist = [
('public', True),
('private', False),
('shared', False),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@ -116,7 +116,6 @@ class TestImageList(TestImage):
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
public=True,
)
@ -136,6 +135,7 @@ class TestImageList(TestImage):
verifylist = [
('public', False),
('private', True),
('shared', False),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@ -143,7 +143,6 @@ class TestImageList(TestImage):
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=False,
private=True,
)
@ -156,6 +155,33 @@ class TestImageList(TestImage):
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_shared_option(self):
arglist = [
'--shared',
]
verifylist = [
('public', False),
('private', False),
('shared', True),
('long', False),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
shared=True,
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))
def test_image_list_long_option(self):
arglist = [
'--long',
@ -167,12 +193,20 @@ class TestImageList(TestImage):
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with(
detailed=True,
)
self.api_mock.image_list.assert_called_with()
collist = ('ID', 'Name', 'Disk Format', 'Container Format',
'Size', 'Status')
collist = (
'ID',
'Name',
'Disk Format',
'Container Format',
'Size',
'Status',
'Visibility',
'Protected',
'Owner',
'Tags',
)
self.assertEqual(collist, columns)
datalist = ((
@ -182,5 +216,42 @@ class TestImageList(TestImage):
'',
'',
'',
'',
False,
image_fakes.image_owner,
'',
), )
self.assertEqual(datalist, tuple(data))
@mock.patch('openstackclient.api.utils.simple_filter')
def test_image_list_property_option(self, sf_mock):
sf_mock.return_value = [
copy.deepcopy(image_fakes.IMAGE),
]
arglist = [
'--property', 'a=1',
]
verifylist = [
('property', {'a': '1'}),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
# DisplayCommandBase.take_action() returns two tuples
columns, data = self.cmd.take_action(parsed_args)
self.api_mock.image_list.assert_called_with()
sf_mock.assert_called_with(
[image_fakes.IMAGE],
attr='a',
value='1',
property_field='properties',
)
collist = ('ID', 'Name')
self.assertEqual(columns, collist)
datalist = ((
image_fakes.image_id,
image_fakes.image_name,
), )
self.assertEqual(datalist, tuple(data))