Add sort_key and sort_dir parameters to *-list

These arguments are using for sorting the result on the server.
Small refactoring was conducted to seperate processing of
common arguments for all lists into the utility function.

Change-Id: I33be1fcecc665c8f2207e53b24ff47954d80dc60
Closes-Bug: #1339731
This commit is contained in:
Dmitry Tantsur
2014-08-15 16:12:19 +02:00
parent 88ad6ddbae
commit b416865ba3
11 changed files with 393 additions and 70 deletions

View File

@@ -104,3 +104,47 @@ def args_array_to_patch(op, attributes):
else:
raise exc.CommandError(_('Unknown PATCH operation: %s') % op)
return patch
def common_params_for_list(args, fields, field_labels):
params = {}
if args.marker is not None:
params['marker'] = args.marker
if args.limit is not None:
params['limit'] = args.limit
if args.sort_key is not None:
# Support using both heading and field name for sort_key
fields_map = dict(zip(field_labels, fields))
fields_map.update(zip(fields, fields))
try:
sort_key = fields_map[args.sort_key]
except KeyError:
raise exc.CommandError(
_("%(sort_key)s is not a valid field for sorting, "
"valid are %(valid)s") %
{'sort_key': args.sort_key,
'valid': list(fields_map)})
params['sort_key'] = sort_key
if args.sort_dir is not None:
if args.sort_dir not in ('asc', 'desc'):
raise exc.CommandError(
_("%s is not valid value for sort direction, "
"valid are 'asc' and 'desc'") %
args.sort_dir)
params['sort_dir'] = args.sort_dir
return params
def common_filters(marker, limit, sort_key, sort_dir):
filters = []
if isinstance(limit, int) and limit > 0:
filters.append('limit=%s' % limit)
if marker is not None:
filters.append('marker=%s' % marker)
if sort_key is not None:
filters.append('sort_key=%s' % sort_key)
if sort_dir is not None:
filters.append('sort_dir=%s' % sort_dir)
return filters

View File

@@ -15,6 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
from ironicclient.common import utils
from ironicclient import exc
from ironicclient.tests import utils as test_utils
@@ -64,3 +66,50 @@ class UtilsTest(test_utils.BaseTestCase):
my_args['attributes'])
self.assertEqual([{'op': 'remove', 'path': '/foo'},
{'op': 'remove', 'path': '/extra/bar'}], patch)
class CommonParamsForListTest(test_utils.BaseTestCase):
def setUp(self):
super(CommonParamsForListTest, self).setUp()
self.args = mock.Mock(marker=None, limit=None,
sort_key=None, sort_dir=None)
def test_nothing_set(self):
self.assertEqual({}, utils.common_params_for_list(self.args, [], []))
def test_marker_and_limit(self):
self.args.marker = 'foo'
self.args.limit = 42
self.assertEqual({'marker': 'foo', 'limit': 42},
utils.common_params_for_list(self.args, [], []))
def test_sort_key_and_sort_dir(self):
self.args.sort_key = 'field'
self.args.sort_dir = 'desc'
self.assertEqual({'sort_key': 'field', 'sort_dir': 'desc'},
utils.common_params_for_list(self.args,
['field'],
[]))
def test_sort_key_allows_label(self):
self.args.sort_key = 'Label'
self.assertEqual({'sort_key': 'field'},
utils.common_params_for_list(self.args,
['field', 'field2'],
['Label', 'Label2']))
def test_sort_key_invalid(self):
self.args.sort_key = 'something'
self.assertRaises(exc.CommandError,
utils.common_params_for_list,
self.args,
['field', 'field2'],
[])
def test_sort_dir_invalid(self):
self.args.sort_dir = 'something'
self.assertRaises(exc.CommandError,
utils.common_params_for_list,
self.args,
[],
[])

View File

@@ -125,6 +125,37 @@ fake_responses_pagination = {
},
}
fake_responses_sorting = {
'/v1/chassis/?sort_key=updated_at':
{
'GET': (
{},
{"chassis": [CHASSIS2]}
),
},
'/v1/chassis/?sort_dir=desc':
{
'GET': (
{},
{"chassis": [CHASSIS2]}
),
},
'/v1/chassis/%s/nodes?sort_key=updated_at' % CHASSIS['uuid']:
{
'GET': (
{},
{"nodes": [NODE]},
),
},
'/v1/chassis/%s/nodes?sort_dir=desc' % CHASSIS['uuid']:
{
'GET': (
{},
{"nodes": [NODE]},
),
},
}
class ChassisManagerTest(testtools.TestCase):
@@ -172,6 +203,26 @@ class ChassisManagerTest(testtools.TestCase):
self.assertEqual(expect, self.api.calls)
self.assertThat(chassis, HasLength(2))
def test_chassis_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.chassis.ChassisManager(self.api)
chassis = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/chassis/?sort_key=updated_at', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(chassis, HasLength(1))
def test_chassis_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.chassis.ChassisManager(self.api)
chassis = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/chassis/?sort_dir=desc', {}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(chassis, HasLength(1))
def test_chassis_show(self):
chassis = self.mgr.get(CHASSIS['uuid'])
expect = [
@@ -229,6 +280,32 @@ class ChassisManagerTest(testtools.TestCase):
self.assertThat(nodes, HasLength(1))
self.assertEqual(NODE['uuid'], nodes[0].uuid)
def test_chassis_node_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.chassis.ChassisManager(self.api)
nodes = self.mgr.list_nodes(CHASSIS['uuid'], sort_key='updated_at')
expect = [
('GET',
'/v1/chassis/%s/nodes?sort_key=updated_at' % CHASSIS['uuid'], {},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(nodes, HasLength(1))
self.assertEqual(NODE['uuid'], nodes[0].uuid)
def test_chassis_node_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.chassis.ChassisManager(self.api)
nodes = self.mgr.list_nodes(CHASSIS['uuid'], sort_dir='desc')
expect = [
('GET',
'/v1/chassis/%s/nodes?sort_dir=desc' % CHASSIS['uuid'], {},
None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(nodes, HasLength(1))
self.assertEqual(NODE['uuid'], nodes[0].uuid)
def test_chassis_node_list_marker(self):
self.api = utils.FakeAPI(fake_responses_pagination)
self.mgr = ironicclient.v1.chassis.ChassisManager(self.api)

View File

@@ -271,6 +271,37 @@ fake_responses_pagination = {
},
}
fake_responses_sorting = {
'/v1/nodes/?sort_key=updated_at':
{
'GET': (
{},
{"nodes": [NODE2, NODE1]}
),
},
'/v1/nodes/?sort_dir=desc':
{
'GET': (
{},
{"nodes": [NODE2, NODE1]}
),
},
'/v1/nodes/%s/ports?sort_key=updated_at' % NODE1['uuid']:
{
'GET': (
{},
{"ports": [PORT]},
),
},
'/v1/nodes/%s/ports?sort_dir=desc' % NODE1['uuid']:
{
'GET': (
{},
{"ports": [PORT]},
),
},
}
class NodeManagerTest(testtools.TestCase):
@@ -318,6 +349,26 @@ class NodeManagerTest(testtools.TestCase):
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(nodes))
def test_node_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = node.NodeManager(self.api)
nodes = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/nodes/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(nodes))
def test_node_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = node.NodeManager(self.api)
nodes = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/nodes/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(nodes))
def test_node_list_associated(self):
nodes = self.mgr.list(associated=True)
expect = [
@@ -449,6 +500,32 @@ class NodeManagerTest(testtools.TestCase):
self.assertEqual(expect, self.api.calls)
self.assertThat(ports, HasLength(1))
def test_node_port_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = node.NodeManager(self.api)
ports = self.mgr.list_ports(NODE1['uuid'], sort_key='updated_at')
expect = [
('GET', '/v1/nodes/%s/ports?sort_key=updated_at' % NODE1['uuid'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(ports, HasLength(1))
self.assertEqual(PORT['uuid'], ports[0].uuid)
self.assertEqual(PORT['address'], ports[0].address)
def test_node_port_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = node.NodeManager(self.api)
ports = self.mgr.list_ports(NODE1['uuid'], sort_dir='desc')
expect = [
('GET', '/v1/nodes/%s/ports?sort_dir=desc' % NODE1['uuid'],
{}, None),
]
self.assertEqual(expect, self.api.calls)
self.assertThat(ports, HasLength(1))
self.assertEqual(PORT['uuid'], ports[0].uuid)
self.assertEqual(PORT['address'], ports[0].address)
def test_node_set_power_state(self):
power_state = self.mgr.set_power_state(NODE1['uuid'], "on")
body = {'target': 'power on'}

View File

@@ -104,6 +104,23 @@ fake_responses_pagination = {
},
}
fake_responses_sorting = {
'/v1/ports/?sort_key=updated_at':
{
'GET': (
{},
{"ports": [PORT2, PORT]}
),
},
'/v1/ports/?sort_dir=desc':
{
'GET': (
{},
{"ports": [PORT2, PORT]}
),
},
}
class PortManagerTest(testtools.TestCase):
@@ -151,6 +168,26 @@ class PortManagerTest(testtools.TestCase):
self.assertEqual(expect, self.api.calls)
self.assertThat(ports, HasLength(2))
def test_ports_list_sort_key(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.port.PortManager(self.api)
ports = self.mgr.list(sort_key='updated_at')
expect = [
('GET', '/v1/ports/?sort_key=updated_at', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(ports))
def test_ports_list_sort_dir(self):
self.api = utils.FakeAPI(fake_responses_sorting)
self.mgr = ironicclient.v1.port.PortManager(self.api)
ports = self.mgr.list(sort_dir='desc')
expect = [
('GET', '/v1/ports/?sort_dir=desc', {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(2, len(ports))
def test_ports_show(self):
port = self.mgr.get(PORT['uuid'])
expect = [

View File

@@ -15,6 +15,7 @@
# under the License.
from ironicclient.common import base
from ironicclient.common import utils
from ironicclient import exc
@@ -33,7 +34,7 @@ class ChassisManager(base.Manager):
def _path(id=None):
return '/v1/chassis/%s' % id if id else '/v1/chassis'
def list(self, marker=None, limit=None):
def list(self, marker=None, limit=None, sort_key=None, sort_dir=None):
"""Retrieve a list of chassis.
:param marker: Optional, the UUID of a chassis, eg the last
@@ -48,17 +49,18 @@ class ChassisManager(base.Manager):
returned respect the maximum imposed by the Ironic API
(see Ironic's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:returns: A list of chassis.
"""
if limit is not None:
limit = int(limit)
filters = []
if isinstance(limit, int) and limit > 0:
filters.append('limit=%s' % limit)
if marker is not None:
filters.append('marker=%s' % marker)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
path = None
if filters:
@@ -70,7 +72,8 @@ class ChassisManager(base.Manager):
return self._list_pagination(self._path(path), "chassis",
limit=limit)
def list_nodes(self, chassis_id, marker=None, limit=None):
def list_nodes(self, chassis_id, marker=None, limit=None,
sort_key=None, sort_dir=None):
"""List all the nodes for a given chassis.
:param chassis_id: The UUID of the chassis.
@@ -86,17 +89,18 @@ class ChassisManager(base.Manager):
returned respect the maximum imposed by the Ironic API
(see Ironic's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:returns: A list of nodes.
"""
if limit is not None:
limit = int(limit)
filters = []
if isinstance(limit, int) and limit > 0:
filters.append('limit=%s' % limit)
if marker is not None:
filters.append('marker=%s' % marker)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
path = "%s/nodes" % chassis_id
if filters:

View File

@@ -45,17 +45,22 @@ def do_chassis_show(cc, args):
help='Chassis UUID (e.g of the last chassis in the list '
'from a previous request). Returns the list of chassis '
'after this UUID.')
@cliutils.arg(
'--sort-key',
metavar='<sort_key>',
help='Chassis field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<sort_dir>',
choices=['asc', 'desc'],
help='Sort direction: one of "asc" (the default) or "desc".')
def do_chassis_list(cc, args):
"""List chassis."""
params = {}
if args.marker is not None:
params['marker'] = args.marker
if args.limit is not None:
params['limit'] = args.limit
chassis = cc.chassis.list(**params)
field_labels = ['UUID', 'Description']
fields = ['uuid', 'description']
params = utils.common_params_for_list(args, fields, field_labels)
chassis = cc.chassis.list(**params)
cliutils.print_list(chassis, fields,
field_labels=field_labels,
sortby_index=None)
@@ -130,19 +135,24 @@ def do_chassis_update(cc, args):
help='Node UUID (e.g of the last node in the list from '
'a previous request). Returns the list of nodes '
'after this UUID.')
@cliutils.arg(
'--sort-key',
metavar='<sort_key>',
help='Node field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<sort_dir>',
choices=['asc', 'desc'],
help='Sort direction: one of "asc" (the default) or "desc".')
@cliutils.arg('chassis', metavar='<chassis id>', help="UUID of chassis")
def do_chassis_node_list(cc, args):
"""List the nodes contained in the chassis."""
params = {}
if args.marker is not None:
params['marker'] = args.marker
if args.limit is not None:
params['limit'] = args.limit
nodes = cc.chassis.list_nodes(args.chassis, **params)
field_labels = ['UUID', 'Instance UUID',
'Power State', 'Provisioning State']
fields = ['uuid', 'instance_uuid', 'power_state', 'provision_state']
params = utils.common_params_for_list(args, fields, field_labels)
nodes = cc.chassis.list_nodes(args.chassis, **params)
cliutils.print_list(nodes, fields,
field_labels=field_labels,
sortby_index=None)

View File

@@ -15,6 +15,7 @@
# under the License.
from ironicclient.common import base
from ironicclient.common import utils
from ironicclient import exc
CREATION_ATTRIBUTES = ['chassis_uuid', 'driver', 'driver_info', 'extra',
@@ -34,7 +35,7 @@ class NodeManager(base.Manager):
return '/v1/nodes/%s' % id if id else '/v1/nodes'
def list(self, associated=None, maintenance=None, marker=None, limit=None,
detail=False):
detail=False, sort_key=None, sort_dir=None):
"""Retrieve a list of nodes.
:param associated: Optional, boolean whether to return a list of
@@ -57,17 +58,18 @@ class NodeManager(base.Manager):
:param detail: Optional, boolean whether to return detailed information
about nodes.
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:returns: A list of nodes.
"""
if limit is not None:
limit = int(limit)
filters = []
if isinstance(limit, int) and limit > 0:
filters.append('limit=%s' % limit)
if marker is not None:
filters.append('marker=%s' % marker)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
if associated is not None:
filters.append('associated=%s' % associated)
if maintenance is not None:
@@ -85,7 +87,8 @@ class NodeManager(base.Manager):
return self._list_pagination(self._path(path), "nodes",
limit=limit)
def list_ports(self, node_id, marker=None, limit=None):
def list_ports(self, node_id, marker=None, limit=None, sort_key=None,
sort_dir=None):
"""List all the ports for a given node.
:param node_id: The UUID of the node.
@@ -101,17 +104,18 @@ class NodeManager(base.Manager):
returned respect the maximum imposed by the Ironic API
(see Ironic's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:returns: A list of ports.
"""
if limit is not None:
limit = int(limit)
filters = []
if isinstance(limit, int) and limit > 0:
filters.append('limit=%s' % limit)
if marker is not None:
filters.append('marker=%s' % marker)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
path = "%s/ports" % node_id
if filters:

View File

@@ -74,6 +74,15 @@ def do_node_show(cc, args):
help='Node UUID (e.g of the last node in the list from '
'a previous request). Returns the list of nodes '
'after this UUID.')
@cliutils.arg(
'--sort-key',
metavar='<sort_key>',
help='Node field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<sort_dir>',
choices=['asc', 'desc'],
help='Sort direction: one of "asc" (the default) or "desc".')
@cliutils.arg(
'--maintenance',
metavar='<maintenance>',
@@ -97,22 +106,22 @@ def do_node_list(cc, args):
params['associated'] = args.associated
if args.maintenance is not None:
params['maintenance'] = args.maintenance
if args.marker is not None:
params['marker'] = args.marker
if args.limit is not None:
params['limit'] = args.limit
params['detail'] = args.detail
nodes = cc.node.list(**params)
if args.detail:
cliutils.print_list(nodes, FIELDS,
field_labels=FIELD_LABELS,
sortby_index=None)
fields = FIELDS
field_labels = FIELD_LABELS
else:
cliutils.print_list(nodes,
LIST_FIELDS,
field_labels=LIST_FIELD_LABELS,
sortby_index=None)
fields = LIST_FIELDS
field_labels = LIST_FIELD_LABELS
params.update(utils.common_params_for_list(args,
fields,
field_labels))
nodes = cc.node.list(**params)
cliutils.print_list(nodes, fields,
field_labels=field_labels,
sortby_index=None)
@cliutils.arg(
@@ -228,18 +237,23 @@ def do_node_vendor_passthru(cc, args):
help='Port UUID (e.g of the last port in the list from '
'a previous request). Returns the list of ports '
'after this UUID.')
@cliutils.arg(
'--sort-key',
metavar='<sort_key>',
help='Port field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<sort_dir>',
choices=['asc', 'desc'],
help='Sort direction: one of "asc" (the default) or "desc".')
@cliutils.arg('node', metavar='<node id>', help="UUID of node")
def do_node_port_list(cc, args):
"""List the ports associated with the node."""
params = {}
if args.marker is not None:
params['marker'] = args.marker
if args.limit is not None:
params['limit'] = args.limit
ports = cc.node.list_ports(args.node, **params)
field_labels = ['UUID', 'Address']
fields = ['uuid', 'address']
params = utils.common_params_for_list(args, fields, field_labels)
ports = cc.node.list_ports(args.node, **params)
cliutils.print_list(ports, fields,
field_labels=field_labels,
sortby_index=None)

View File

@@ -15,6 +15,7 @@
# under the License.
from ironicclient.common import base
from ironicclient.common import utils
from ironicclient import exc
CREATION_ATTRIBUTES = ['address', 'extra', 'node_uuid']
@@ -32,7 +33,7 @@ class PortManager(base.Manager):
def _path(id=None):
return '/v1/ports/%s' % id if id else '/v1/ports'
def list(self, limit=None, marker=None):
def list(self, limit=None, marker=None, sort_key=None, sort_dir=None):
"""Retrieve a list of port.
:param marker: Optional, the UUID of a port, eg the last
@@ -47,17 +48,18 @@ class PortManager(base.Manager):
returned respect the maximum imposed by the Ironic API
(see Ironic's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:returns: A list of ports.
"""
if limit is not None:
limit = int(limit)
filters = []
if isinstance(limit, int) and limit > 0:
filters.append('limit=%s' % limit)
if marker is not None:
filters.append('marker=%s' % marker)
filters = utils.common_filters(marker, limit, sort_key, sort_dir)
path = None
if filters:

View File

@@ -55,17 +55,22 @@ def do_port_show(cc, args):
help='Port UUID (e.g of the last port in the list from '
'a previous request). Returns the list of ports '
'after this UUID.')
@cliutils.arg(
'--sort-key',
metavar='<sort_key>',
help='Port field that will be used for sorting.')
@cliutils.arg(
'--sort-dir',
metavar='<sort_dir>',
choices=['asc', 'desc'],
help='Sort direction: one of "asc" (the default) or "desc".')
def do_port_list(cc, args):
"""List ports."""
params = {}
if args.marker is not None:
params['marker'] = args.marker
if args.limit is not None:
params['limit'] = args.limit
port = cc.port.list(**params)
field_labels = ['UUID', 'Address']
fields = ['uuid', 'address']
params = utils.common_params_for_list(args, fields, field_labels)
port = cc.port.list(**params)
cliutils.print_list(port, fields,
field_labels=field_labels,
sortby_index=None)