diff --git a/cratonclient/exceptions.py b/cratonclient/exceptions.py index 0c849f4..b0b17f4 100644 --- a/cratonclient/exceptions.py +++ b/cratonclient/exceptions.py @@ -75,6 +75,12 @@ class HTTPError(ClientException): self.status_code = code +class CommandError(ClientException): + """Client command was invalid or failed.""" + + message = "The command used was invalid or caused an error.""" + + class ConnectionFailed(HTTPError): """Connecting to the server failed.""" diff --git a/cratonclient/shell/v1/hosts_shell.py b/cratonclient/shell/v1/hosts_shell.py index 888b064..3c3b31c 100644 --- a/cratonclient/shell/v1/hosts_shell.py +++ b/cratonclient/shell/v1/hosts_shell.py @@ -13,11 +13,73 @@ # under the License. """Hosts resource and resource shell wrapper.""" from cratonclient.common import cliutils +from cratonclient import exceptions as exc +from cratonclient.v1.hosts import HOST_FIELDS as h_fields +@cliutils.arg('-c', '--cell', + metavar='', + type=int, + help='Integer ID of the cell that contains ' + 'the desired list of hosts.') +@cliutils.arg('--detail', + action='store_true', + default=False, + help='Show detailed information about the hosts.') +@cliutils.arg('--limit', + metavar='', + type=int, + help='Maximum number of hosts to return.') +@cliutils.arg('--sort-key', + metavar='', + help='Host field that will be used for sorting.') +@cliutils.arg('--sort-dir', + metavar='', + default='asc', + help='Sort direction: "asc" (default) or "desc".') +@cliutils.arg('--fields', + nargs='+', + metavar='', + default=[], + help='Comma-separated list of fields to display. ' + 'Only these fields will be fetched from the server. ' + 'Can not be used when "--detail" is specified') def do_host_list(cc, args): """Print list of hosts which are registered with the Craton service.""" params = {} - columns = ['id', 'name'] + default_fields = ['id', 'name', 'type', 'active', 'cell_id'] + if args.cell is not None: + params['cell'] = args.cell + if args.limit is not None: + if args.limit < 0: + raise exc.CommandError('Invalid limit specified. Expected ' + 'non-negative limit, got {0}' + .format(args.limit)) + params['limit'] = args.limit + if args.detail: + fields = h_fields + params['detail'] = args.detail + elif args.fields: + fields = {x: h_fields[x] for x in args.fields} + else: + fields = {x: h_fields[x] for x in default_fields} + if args.sort_key is not None: + fields_map = dict(zip(fields.keys(), fields.keys())) + # TODO(cmspence): Do we want to allow sorting by field heading value? + try: + sort_key = fields_map[args.sort_key] + except KeyError: + raise exc.CommandError( + '{0} is an invalid key for sorting, valid values for ' + '--sort-key are: {1}'.format(args.sort_key, h_fields.keys()) + ) + params['sort_key'] = sort_key + if args.sort_dir is not None: + if args.sort_dir not in ('asc', 'desc'): + raise exc.CommandError('Invalid sort direction specified. The ' + 'expected valid values for --sort-dir ' + 'are: "asc", "desc".') + params['sort_dir'] = args.sort_dir + hosts = cc.hosts.list(args.craton_project_id, **params) - cliutils.print_list(hosts, columns) + cliutils.print_list(hosts, list(fields)) diff --git a/cratonclient/tests/unit/test_hosts_shell.py b/cratonclient/tests/unit/test_hosts_shell.py index 9a0d892..00678c1 100644 --- a/cratonclient/tests/unit/test_hosts_shell.py +++ b/cratonclient/tests/unit/test_hosts_shell.py @@ -14,6 +14,7 @@ import mock +from cratonclient import exceptions as exc from cratonclient.tests import base @@ -25,3 +26,105 @@ class TestHostsShell(base.ShellTestCase): """Verify that no arguments prints out all project hosts.""" self.shell('host-list') self.assertTrue(mock_list.called) + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_parse_param_success(self, mock_list): + """Verify that success of parsing a subcommand argument.""" + self.shell('host-list --limit 0') + self.assertTrue(mock_list.called) + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_limit_0_succcess(self, mock_list): + """Verify that --limit 0 prints out all project hosts.""" + self.shell('host-list --limit 0') + mock_list.assert_called_once_with(mock.ANY, limit=0) + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_limit_positive_num_success(self, mock_list): + """Verify --limit X, where X is a positive integer, succeeds. + + The command will print out X number of project hosts. + """ + self.shell('host-list --limit 1') + mock_list.assert_called_once_with(mock.ANY, limit=1) + + def test_host_list_limit_negative_num_failure(self): + """Verify --limit X, where X is a negative integer, fails. + + The command will cause a Command Error message response. + """ + self.assertRaises(exc.CommandError, + self.shell, + 'host-list --limit -1') + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_cell_success(self, mock_list): + """Verify --cell arguments successfully pass cell to Client.""" + for cell_arg in ['-c', '--cell']: + self.shell('host-list {0} 1'.format(cell_arg)) + mock_list.assert_called_once_with(mock.ANY, cell=1) + mock_list.reset_mock() + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_detail_success(self, mock_list): + """Verify --detail argument successfully pass detail to Client.""" + self.shell('host-list --detail') + mock_list.assert_called_once_with(mock.ANY, detail=True) + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + @mock.patch('cratonclient.common.cliutils.print_list') + def test_host_list_fields_success(self, mock_printlist, mock_list): + """Verify --fields argument successfully passed to Client.""" + self.shell('host-list --fields id name') + mock_list.assert_called_once_with(mock.ANY) + mock_printlist.assert_called_once_with(mock.ANY, + list({'id': 'ID', + 'name': 'Name'})) + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_detail_and_fields_specified(self, mock_list): + """Verify --fields ignored when --detail argument passed in.""" + self.shell('host-list --fields id name --detail') + mock_list.assert_called_once_with(mock.ANY, detail=True) + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_sort_key_field_key_success(self, mock_list): + """Verify --sort-key arguments successfully passed to Client.""" + self.shell('host-list --sort-key cell_id') + mock_list.assert_called_once_with(mock.ANY, + sort_key='cell_id', + sort_dir='asc') + + def test_host_list_sort_key_invalid(self): + """Verify --sort-key with invalid args, fails with Command Error.""" + self.assertRaises(exc.CommandError, + self.shell, + 'host-list --sort-key invalid') + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_sort_dir_not_passed_without_sort_key(self, mock_list): + """Verify --sort-dir arg ignored without --sort-key.""" + self.shell('host-list --sort-dir desc') + mock_list.assert_called_once_with(mock.ANY) + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_sort_dir_asc_success(self, mock_list): + """Verify --sort-dir asc successfully passed to Client.""" + self.shell('host-list --sort-key name --sort-dir asc') + mock_list.assert_called_once_with(mock.ANY, + sort_key='name', + sort_dir='asc') + + @mock.patch('cratonclient.v1.hosts.HostManager.list') + def test_host_list_sort_dir_desc_success(self, mock_list): + """Verify --sort-dir desc successfully passed to Client.""" + self.shell('host-list --sort-key name --sort-dir desc') + mock_list.assert_called_once_with(mock.ANY, + sort_key='name', + sort_dir='desc') + + def test_host_list_sort_dir_invalid_value(self): + """Verify --sort-dir with invalid args, fails with Command Error.""" + self.assertRaises(exc.CommandError, + self.shell, + 'host-list --sort-key name --sort-dir invalid') diff --git a/cratonclient/v1/hosts.py b/cratonclient/v1/hosts.py index 9f3e17f..078bb43 100644 --- a/cratonclient/v1/hosts.py +++ b/cratonclient/v1/hosts.py @@ -32,3 +32,18 @@ class HostManager(crud.CRUDClient): """Retrieve the hosts in a specific region.""" kwargs['project'] = str(project_id) super(HostManager, self).list(**kwargs) + +HOST_FIELDS = { + 'id': 'ID', + 'name': 'Name', + 'type': 'Type', + 'project_id': 'Project ID', + 'region_id': 'Region ID', + 'cell_id': 'Cell ID', + 'ip_address': 'IP Address', + 'active': 'Active', + 'note': 'Note', + 'access_secret_id': "Access Secret ID", + 'created_at': 'Created At', + 'update_at': 'Updated At' +}