From 4b530bf9d629a93324af63da1fc4afe997c4e9b6 Mon Sep 17 00:00:00 2001 From: Steven Kaufer Date: Thu, 28 Aug 2014 02:58:58 +0000 Subject: [PATCH] novaclient sort parameters support Adds sorting support to the 'nova list' command. --sort [:] The --sort parameter is comma-separated and is used to specify one or more sort keys and directions. The direction defaults to 'desc' for each sort key and the user can supply 'asc' to override. Partially implements: blueprint nova-pagination Change-Id: I635e017c7f9ab61812333983bfecccd6fce8d394 --- novaclient/tests/v1_1/test_servers.py | 19 ++++++++++ novaclient/tests/v1_1/test_shell.py | 54 +++++++++++++++++++++++++++ novaclient/v1_1/servers.py | 17 +++++++-- novaclient/v1_1/shell.py | 29 +++++++++++++- 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py index 3797e5754..c4037e1b3 100644 --- a/novaclient/tests/v1_1/test_servers.py +++ b/novaclient/tests/v1_1/test_servers.py @@ -49,6 +49,25 @@ class ServersTest(utils.FixturedTestCase): for s in sl: self.assertIsInstance(s, servers.Server) + def test_list_servers_sort_single(self): + sl = self.cs.servers.list(sort_keys=['display_name'], + sort_dirs=['asc']) + self.assert_called( + 'GET', + '/servers/detail?sort_dir=asc&sort_key=display_name') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_sort_multiple(self): + sl = self.cs.servers.list(sort_keys=['display_name', 'id'], + sort_dirs=['asc', 'desc']) + self.assert_called( + 'GET', + ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=display_name&sort_key=id')) + for s in sl: + self.assertIsInstance(s, servers.Server) + def test_get_server_details(self): s = self.cs.servers.get(1234) self.assert_called('GET', '/servers/1234') diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py index 622e082fc..c1b5e1af4 100644 --- a/novaclient/tests/v1_1/test_shell.py +++ b/novaclient/tests/v1_1/test_shell.py @@ -851,6 +851,60 @@ class ShellTest(utils.TestCase): 'GET', '/servers/detail?all_tenants=1&user_id=fake_user') + def test_list_with_single_sort_key_no_dir(self): + self.run_command('list --sort 1') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_key=1')) + + def test_list_with_single_sort_key_and_dir(self): + self.run_command('list --sort 1:asc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=asc&sort_key=1')) + + def test_list_with_sort_keys_no_dir(self): + self.run_command('list --sort 1,2') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_dir=desc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_sort_keys_and_dirs(self): + self.run_command('list --sort 1:asc,2:desc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_sort_keys_and_some_dirs(self): + self.run_command('list --sort 1,2:asc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_dir=asc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_invalid_sort_dir_one(self): + cmd = 'list --sort 1:foo' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_list_with_invalid_sort_dir_two(self): + cmd = 'list --sort 1:asc,2:foo' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_list_sortby_index_with_sort(self): + # sortby_index is None if there is sort information + for cmd in ['list --sort key', + 'list --sort key:desc', + 'list --sort key1,key2:asc']: + with mock.patch('novaclient.utils.print_list') as mock_print_list: + self.run_command(cmd) + mock_print_list.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, sortby_index=None) + + def test_list_sortby_index_without_sort(self): + # sortby_index is 1 without sort information + for cmd in ['list', 'list --minimal', 'list --deleted']: + with mock.patch('novaclient.utils.print_list') as mock_print_list: + self.run_command(cmd) + mock_print_list.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, sortby_index=1) + def test_list_fields(self): output = self.run_command( 'list --fields ' diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py index 2fe29afab..f1b10a997 100644 --- a/novaclient/v1_1/servers.py +++ b/novaclient/v1_1/servers.py @@ -566,7 +566,8 @@ class ServerManager(base.BootingManagerWithFind): """ return self._get("/servers/%s" % base.getid(server), "server") - def list(self, detailed=True, search_opts=None, marker=None, limit=None): + def list(self, detailed=True, search_opts=None, marker=None, limit=None, + sort_keys=None, sort_dirs=None): """ Get a list of servers. @@ -575,6 +576,8 @@ class ServerManager(base.BootingManagerWithFind): :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). + :param sort_keys: List of sort keys + :param sort_dirs: List of sort directions :rtype: list of :class:`Server` """ @@ -595,8 +598,16 @@ class ServerManager(base.BootingManagerWithFind): # Transform the dict to a sequence of two-element tuples in fixed # order, then the encoded string will be consistent in Python 2&3. - if qparams: - new_qparams = sorted(qparams.items(), key=lambda x: x[0]) + if qparams or sort_keys or sort_dirs: + # sort keys and directions are unique since the same parameter + # key is repeated for each associated value + # (ie, &sort_key=key1&sort_key=key2&sort_key=key3) + items = list(qparams.items()) + if sort_keys: + items.extend(('sort_key', sort_key) for sort_key in sort_keys) + if sort_dirs: + items.extend(('sort_dir', sort_dir) for sort_dir in sort_dirs) + new_qparams = sorted(items, key=lambda x: x[0]) query_string = "?%s" % parse.urlencode(new_qparams) else: query_string = "" diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index ddf9de979..3a438fdff 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -1308,6 +1308,13 @@ def do_image_delete(cs, args): action="store_true", default=False, help=_('Get only uuid and name.')) +@cliutils.arg( + '--sort', + dest='sort', + metavar='[:]', + help=('Comma-separated list of sort keys and directions in the form' + ' of [:]. The direction defaults to descending if' + ' not specified.')) def do_list(cs, args): """List active servers.""" imageid = None @@ -1350,8 +1357,23 @@ def do_list(cs, args): detailed = not args.minimal + sort_keys = [] + sort_dirs = [] + if args.sort: + for sort in args.sort.split(','): + sort_key, _sep, sort_dir = sort.partition(':') + if not sort_dir: + sort_dir = 'desc' + elif sort_dir not in ('asc', 'desc'): + raise exceptions.CommandError(_( + 'Unknown sort direction: %s') % sort_dir) + sort_keys.append(sort_key) + sort_dirs.append(sort_dir) + servers = cs.servers.list(detailed=detailed, - search_opts=search_opts) + search_opts=search_opts, + sort_keys=sort_keys, + sort_dirs=sort_dirs) convert = [('OS-EXT-SRV-ATTR:host', 'host'), ('OS-EXT-STS:task_state', 'task_state'), ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), @@ -1375,8 +1397,11 @@ def do_list(cs, args): 'Networks' ] formatters['Networks'] = utils._format_servers_list_networks + sortby_index = 1 + if args.sort: + sortby_index = None utils.print_list(servers, columns, - formatters, sortby_index=1) + formatters, sortby_index=sortby_index) @cliutils.arg(