diff --git a/tricircleclient/constants.py b/tricircleclient/constants.py index 01ba83f..dd76400 100644 --- a/tricircleclient/constants.py +++ b/tricircleclient/constants.py @@ -44,9 +44,9 @@ job_resource_map = { # listed by alphabet order. COLUMNS = ('id', 'project_id', 'status', 'timestamp', 'type') -# column headers that show in command line. -COLUMNS_REMAP = {'id': 'Id', - 'project_id': 'Project id', +# column headers about job that show in command line. +COLUMNS_REMAP = {'id': 'ID', + 'project_id': 'Project', 'timestamp': 'Timestamp', 'status': 'Status', 'type': 'Type'} diff --git a/tricircleclient/tests/unit/v1/test_jobs_cli.py b/tricircleclient/tests/unit/v1/test_jobs_cli.py index 18a87c7..686d62a 100644 --- a/tricircleclient/tests/unit/v1/test_jobs_cli.py +++ b/tricircleclient/tests/unit/v1/test_jobs_cli.py @@ -67,7 +67,7 @@ class TestShowJob(_TestJobCommand, utils.TestCommandWithoutOptions): _job['job']['id'], ] verifylist = [ - ('id', _job['job']['id']), + ('job', _job['job']['id']), ] self.job_manager.get = mock.Mock(return_value=_job) parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -88,7 +88,6 @@ class TestListJob(_TestJobCommand): self.job_manager.list = mock.Mock(return_value={'jobs': _jobs}) parsed_args = self.check_parser(self.cmd) columns, data = (self.cmd.take_action(parsed_args)) - self.assertEqual(sorted(constants.COLUMNS_REMAP.values()), sorted(columns)) self.assertEqual(len(_jobs), len(data)) @@ -96,6 +95,96 @@ class TestListJob(_TestJobCommand): sorted([tuple(o[k] for k in constants.COLUMNS) for o in _jobs]), sorted(data)) + def test_list_with_filters(self): + _job = utils.FakeJob.create_single_job() + _job = _job['job'] + + # we filter the jobs by the following fields: project ID, type, status. + # given values of _job, then only single item _job is retrieved. + arglist = [ + '--project-id', _job['project_id'], + '--type', _job['type'], + '--status', _job['status'], + ] + verifylist = [ + ('project_id', _job['project_id']), + ('type', _job['type']), + ('status', _job['status'].lower()), + ] + + self.job_manager.list = mock.Mock(return_value={'jobs': [_job]}) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.assertEqual(1, len(data)) + self.assertEqual(sorted(constants.COLUMNS_REMAP.values()), + sorted(columns)) + + # lower case of job status + arglist = [ + '--status', _job['status'].lower(), + ] + verifylist = [ + ('status', _job['status'].lower()), + ] + + self.job_manager.list = mock.Mock(return_value={'jobs': [_job]}) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.assertEqual(1, len(data)) + self.assertEqual(sorted(constants.COLUMNS_REMAP.values()), + sorted(columns)) + + def test_invalid_job_status_filter(self): + # unrecognizable job status filter + arglist = [ + '--status', 'new_1', + ] + verifylist = [] + self.assertRaises(utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_list_with_pagination(self): + number_of_jobs = 4 + limit = number_of_jobs - 2 + _jobs = utils.FakeJob.create_multiple_jobs(count=number_of_jobs) + + # test list operation with pagination + arglist = [ + '--limit', str(limit), + ] + verifylist = [ + ('limit', limit), + ] + + self.job_manager.list = mock.Mock(return_value={"jobs": _jobs[:limit]}) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.assertEqual(limit, len(data)) + self.assertEqual(sorted(constants.COLUMNS_REMAP.values()), + sorted(columns)) + + # test list operation with pagination and marker + arglist = [ + '--limit', str(limit), + '--marker', _jobs[0]['id'], + ] + verifylist = [ + ('limit', limit), + ('marker', _jobs[0]['id']), + ] + + self.job_manager.list = mock.Mock( + return_value={"jobs": _jobs[1:limit+1]}) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self.assertEqual(limit, len(data)) + self.assertEqual(sorted(constants.COLUMNS_REMAP.values()), + sorted(columns)) + class TestDeleteJob(_TestJobCommand, utils.TestCommandWithoutOptions): @@ -109,7 +198,7 @@ class TestDeleteJob(_TestJobCommand, utils.TestCommandWithoutOptions): _job['job']['id'], ] verifylist = [ - ('id', _job['job']['id']), + ('job', [_job['job']['id']]), ] self.job_manager.delete = mock.Mock(return_value=None) parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -130,7 +219,7 @@ class TestRedoJob(_TestJobCommand, utils.TestCommandWithoutOptions): _job['job']['id'], ] verifylist = [ - ('id', _job['job']['id']), + ('job', _job['job']['id']), ] self.job_manager.update = mock.Mock(return_value=None) parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/tricircleclient/tests/unit/v1/test_pods_cli.py b/tricircleclient/tests/unit/v1/test_pods_cli.py index b498e1c..bf79f2c 100644 --- a/tricircleclient/tests/unit/v1/test_pods_cli.py +++ b/tricircleclient/tests/unit/v1/test_pods_cli.py @@ -126,8 +126,8 @@ class TestShowPod(_TestPodCommand, utils.TestCommandWithoutOptions): class TestListPod(_TestPodCommand): columns = [ - 'Id', - 'Region name', + 'ID', + 'Region Name', ] def setUp(self): diff --git a/tricircleclient/tests/unit/v1/test_routings_cli.py b/tricircleclient/tests/unit/v1/test_routings_cli.py index 71cf746..adcf2b3 100644 --- a/tricircleclient/tests/unit/v1/test_routings_cli.py +++ b/tricircleclient/tests/unit/v1/test_routings_cli.py @@ -79,10 +79,10 @@ class TestShowRouting(_TestRoutingCommand, utils.TestCommandWithoutOptions): class TestListRouting(_TestRoutingCommand): columns = [ - 'Id', - 'Pod id', - 'Resource type', - 'Top id' + 'ID', + 'Pod ID', + 'Resource Type', + 'Top ID' ] def setUp(self): @@ -100,8 +100,7 @@ class TestListRouting(_TestRoutingCommand): return_value={'routings': _routings}) parsed_args = self.check_parser(self.cmd) columns, data = (self.cmd.take_action(parsed_args)) - - self.assertEqual(self.columns, sorted(columns)) + self.assertEqual(sorted(self.columns), sorted(columns)) self.assertEqual(len(_routings), len(data)) def test_list_with_filters(self): @@ -151,7 +150,7 @@ class TestListRouting(_TestRoutingCommand): def test_list_with_pagination(self): _routings = utils.FakeRouting.create_multiple_routings(count=3) arglist = [ - '--page-size', '2', + '--limit', '2', '--marker', _routings[0]['id'], ] verifylist = [ @@ -167,7 +166,7 @@ class TestListRouting(_TestRoutingCommand): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = (self.cmd.take_action(parsed_args)) - self.assertEqual(self.columns, sorted(columns)) + self.assertEqual(sorted(self.columns), sorted(columns)) self.assertEqual(2, len(data)) @@ -178,12 +177,14 @@ class TestDeleteRouting(_TestRoutingCommand, utils.TestCommandWithoutOptions): self.cmd = routings_cli.DeleteRouting(self.app, None) def test_delete_routing(self): - _routing = utils.FakeRouting.create_single_routing() + _routings = utils.FakeRouting.create_multiple_routings(count=2) + arglist = [ - _routing['routing']['id'], + _routings[0]['id'], + _routings[1]['id'], ] verifylist = [ - ('routing', [_routing['routing']['id']]), + ('routing', [_routings[0]['id'], _routings[1]['id']]), ] self.routing_manager.delete = mock.Mock(return_value=None) parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/tricircleclient/utils.py b/tricircleclient/utils.py index 473f280..d274272 100644 --- a/tricircleclient/utils.py +++ b/tricircleclient/utils.py @@ -17,7 +17,7 @@ def prepare_column_headers(columns, remap=None): for c in columns: for old, new in remap.items(): c = c.replace(old, new) - new_columns.append(c.replace('_', ' ').capitalize()) + new_columns.append(c.replace('_', ' ')) return new_columns diff --git a/tricircleclient/v1/jobs.py b/tricircleclient/v1/jobs.py index f6d3d06..5a9d69b 100644 --- a/tricircleclient/v1/jobs.py +++ b/tricircleclient/v1/jobs.py @@ -18,10 +18,10 @@ from tricircleclient.v1 import base class JobManager(base.Manager): - def list(self, search_opts=None): + def list(self, path): """Get a list of jobs.""" return self._get( - '/jobs', + path, headers={'Content-Type': "application/json"}).json() def create(self, job): diff --git a/tricircleclient/v1/jobs_cli.py b/tricircleclient/v1/jobs_cli.py index 9d747e3..a77fc41 100644 --- a/tricircleclient/v1/jobs_cli.py +++ b/tricircleclient/v1/jobs_cli.py @@ -13,6 +13,7 @@ from osc_lib.command import command from oslo_log import log as logging +from six.moves.urllib import parse from tricircleclient import constants from tricircleclient import utils @@ -33,6 +34,59 @@ def _job_from_args(parsed_args): return {'job': data} +def _add_pagination_argument(parser): + parser.add_argument( + '--limit', + dest='limit', metavar="", type=int, + help="Maximum number of jobs to return", + default=None) + + +def _add_marker_argument(parser): + parser.add_argument( + '--marker', + dest='marker', metavar="", type=str, + help="ID of last job in previous page, jobs after marker will be " + "returned. Display all jobs if not specified.", + default=None) + + +def _add_filtering_arguments(parser): + # available filtering fields: project ID, type, status + parser.add_argument( + '--project-id', + dest='project_id', metavar="", type=str, + help="ID of a project object in Keystone", + default=None) + parser.add_argument( + '--type', + dest='type', metavar="", type=str, + choices=constants.job_resource_map.keys(), + help="Job type", + default=None) + parser.add_argument( + '--status', + dest='status', metavar="", type=lambda str: str.lower(), + choices=['new', 'running', 'success', 'fail'], + help="Execution status of the job. It's case-insensitive", + default=None) + + +def _add_search_options(parsed_args): + search_opts = {} + for key in ('limit', 'marker', 'project_id', 'type', 'status'): + value = getattr(parsed_args, key, None) + if value is not None: + search_opts[key] = value + return search_opts + + +def _prepare_query_string(params): + """Convert dict params to query string""" + params = sorted(params.items(), key=lambda x: x[0]) + return '?%s' % parse.urlencode(params) if params else '' + + def expand_job_resource(job): # because job['resource'] is a dict value, so we should # expand its values and let them show as other fields in the @@ -47,10 +101,26 @@ class ListJobs(command.Lister): """List Jobs""" log = logging.getLogger(__name__ + ".ListJobs") + path = '/jobs' + + def get_parser(self, prog_name): + parser = super(ListJobs, self).get_parser(prog_name) + + _add_pagination_argument(parser) + _add_marker_argument(parser) + _add_filtering_arguments(parser) + + return parser + def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) client = self.app.client_manager.multiregion_networking - data = client.job.list() + + # add pagination/marker/filter to list operation + search_opts = _add_search_options(parsed_args) + self.path += _prepare_query_string(search_opts) + + data = client.job.list(self.path) column_headers = utils.prepare_column_headers(constants.COLUMNS, constants.COLUMNS_REMAP) return utils.list2cols(constants.COLUMNS, data['jobs'], column_headers) @@ -75,44 +145,44 @@ class CreateJob(command.ShowOne): ) parser.add_argument( '--project_id', - metavar="", + metavar="", required=True, - help="Uuid of a project object in Keystone", + help="ID of a project object in Keystone", ) parser.add_argument( '--router_id', - metavar="", - help="Uuid of a router", + metavar="", + help="ID of a router", ) parser.add_argument( '--network_id', - metavar="", - help="Uuid of a network", + metavar="", + help="ID of a network", ) parser.add_argument( '--pod_id', - metavar="", - help="Uuid of a pod", + metavar="", + help="ID of a pod", ) parser.add_argument( '--port_id', - metavar="", - help="Uuid of a port", + metavar="", + help="ID of a port", ) parser.add_argument( '--trunk_id', - metavar="", - help="Uuid of a trunk", + metavar="", + help="ID of a trunk", ) parser.add_argument( '--subnet_id', - metavar="", - help="Uuid of a subnet", + metavar="", + help="ID of a subnet", ) parser.add_argument( '--portchain_id', - metavar="", - help="Uuid of a port chain", + metavar="", + help="ID of a port chain", ) return parser @@ -132,16 +202,16 @@ class ShowJob(command.ShowOne): def get_parser(self, prog_name): parser = super(ShowJob, self).get_parser(prog_name) parser.add_argument( - "id", - metavar="", - help="Uuid of the job to display", + "job", + metavar="", + help="ID of the job to display", ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) client = self.app.client_manager.multiregion_networking - data = client.job.get(parsed_args.id) + data = client.job.get(parsed_args.job) if 'job' in data.keys(): return self.dict2columns(expand_job_resource(data['job'])) @@ -155,16 +225,18 @@ class DeleteJob(command.Command): def get_parser(self, prog_name): parser = super(DeleteJob, self).get_parser(prog_name) parser.add_argument( - "id", - metavar="", - help="Uuid of the job to delete", + "job", + metavar="", + nargs="+", + help="ID(s) of the job(s) to delete", ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) client = self.app.client_manager.multiregion_networking - client.job.delete(parsed_args.id) + for job_id in parsed_args.job: + client.job.delete(job_id) class RedoJob(command.Command): @@ -176,13 +248,13 @@ class RedoJob(command.Command): parser = super(RedoJob, self).get_parser(prog_name) parser.add_argument( - 'id', - metavar="", - help="Uuid of the job to redo", + 'job', + metavar="", + help="ID of the job to redo", ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) client = self.app.client_manager.multiregion_networking - client.job.update(parsed_args.id) + client.job.update(parsed_args.job) diff --git a/tricircleclient/v1/pods_cli.py b/tricircleclient/v1/pods_cli.py index 9c88d3d..392be2e 100644 --- a/tricircleclient/v1/pods_cli.py +++ b/tricircleclient/v1/pods_cli.py @@ -28,7 +28,7 @@ class ListPods(command.Lister): self.log.debug("take_action(%s)" % parsed_args) client = self.app.client_manager.multiregion_networking data = client.pod.list() - remap = {'pod_id': 'Id', + remap = {'pod_id': 'ID', 'region_name': 'Region Name', 'az_name': 'Availability Zone', 'dc_name': 'Data Center'} diff --git a/tricircleclient/v1/routings_cli.py b/tricircleclient/v1/routings_cli.py index fc83f21..de6856e 100644 --- a/tricircleclient/v1/routings_cli.py +++ b/tricircleclient/v1/routings_cli.py @@ -33,50 +33,50 @@ def _routing_from_args(parsed_args): def _add_pagination_argument(parser): parser.add_argument( - '-P', '--page-size', - dest='limit', metavar='SIZE', type=int, + '--limit', + dest='limit', metavar="", type=int, help="Maximum number of routings to return", default=None) def _add_marker_argument(parser): parser.add_argument( - '-M', '--marker', - dest='marker', metavar='MARKER', type=str, - help="Starting point for next routing list " - "that shares same routing id", + '--marker', + dest='marker', metavar="", type=str, + help="ID of last routing in previous page, routings after marker " + "will be returned. Display all routings if not specified.", default=None) def _add_filtering_arguments(parser): parser.add_argument( - '--id', - dest='id', metavar='id', type=int, - help="Uuid of a routing", + '--routing', + dest='routing', metavar="", type=int, + help="ID of a routing", default=None) parser.add_argument( '--top-id', - dest='top_id', metavar='top_id', type=str, + dest='top_id', metavar="", type=str, help="Resource id on Central Neutron", default=None) parser.add_argument( '--bottom-id', - dest='bottom_id', metavar='bottom_id', type=str, + dest='bottom_id', metavar="", type=str, help="Resource id on Local Neutron", default=None) parser.add_argument( '--pod-id', - dest='pod_id', metavar='pod_id', type=str, - help="Uuid of a pod", + dest='pod_id', metavar="", type=str, + help="ID of a pod", default=None) parser.add_argument( '--project-id', - dest='project_id', metavar='project_id', type=str, - help="Uuid of a project object in Keystone", + dest='project_id', metavar="", type=str, + help="ID of a project object in Keystone", default=None) parser.add_argument( '--resource-type', - dest='resource_type', metavar='resource_type', type=str, + dest='resource_type', metavar="", type=str, choices=['network', 'subnet', 'port', 'router', 'security_group', 'trunk', 'port_pair', 'port_pair_group', 'flow_classifier', 'port_chain'], @@ -84,12 +84,12 @@ def _add_filtering_arguments(parser): default=None) parser.add_argument( '--created-at', - dest='created_at', metavar='created_at', type=str, + dest='created_at', metavar="", type=str, help="Create time of the resource routing", default=None) parser.add_argument( '--updated-at', - dest='updated_at', metavar='updated_at', type=str, + dest='updated_at', metavar="", type=str, help="Update time of the resource routing", default=None) @@ -116,18 +116,13 @@ class ListRoutings(command.Lister): COLS = ('id', 'pod_id', 'resource_type', 'top_id') path = '/routings' - pagination_support = True - marker_support = True - log = logging.getLogger(__name__ + ".ListRoutings") def get_parser(self, prog_name): parser = super(ListRoutings, self).get_parser(prog_name) - if self.pagination_support: - _add_pagination_argument(parser) - if self.marker_support: - _add_marker_argument(parser) + _add_pagination_argument(parser) + _add_marker_argument(parser) _add_filtering_arguments(parser) return parser @@ -141,13 +136,13 @@ class ListRoutings(command.Lister): self.path += _prepare_query_string(search_opts) data = client.routing.list(self.path) - remap = {'id': 'Id', - 'pod_id': 'Pod Id', - 'resource_type': 'Resource Type', - 'top_id': 'Top Id'} + remap = {'resource_type': 'Resource Type', + 'pod': 'Pod', + 'id': 'ID', + 'top': 'Top', + } column_headers = utils.prepare_column_headers(self.COLS, remap) - return utils.list2cols( self.COLS, data['routings'], column_headers) @@ -162,31 +157,31 @@ class CreateRouting(command.ShowOne): parser.add_argument( '--top-id', - metavar="", + metavar="", required=True, help="Resource id on Central Neutron", ) parser.add_argument( '--bottom-id', - metavar="", + metavar="", required=True, help="Resource id on Local Neutron", ) parser.add_argument( '--pod-id', - metavar="", + metavar="", required=True, - help="Uuid of a pod", + help="ID of a pod", ) parser.add_argument( '--project-id', - metavar="", + metavar="", required=True, - help="Uuid of a project object in Keystone", + help="ID of a project object in Keystone", ) parser.add_argument( '--resource-type', - metavar="", + metavar="", choices=['network', 'subnet', 'port', 'router', 'security_group'], required=True, help="Available resource types", @@ -211,7 +206,7 @@ class ShowRouting(command.ShowOne): parser.add_argument( "routing", metavar="", - help="Id of the routing resource to display", + help="ID of the routing resource to display", ) return parser @@ -234,7 +229,7 @@ class DeleteRouting(command.Command): parser = super(DeleteRouting, self).get_parser(prog_name) parser.add_argument( "routing", - metavar="", + metavar="", nargs="+", help="ID(s) of the routing resource(s) to delete", ) @@ -258,33 +253,33 @@ class UpdateRouting(command.Command): parser.add_argument( '--top-id', - metavar="", + metavar="", help="Resource id on Central Neutron", ) parser.add_argument( '--bottom-id', - metavar="", + metavar="", help="Resource id on Local Neutron", ) parser.add_argument( '--pod-id', - metavar="", - help="Uuid of a pod", + metavar="", + help="ID of a pod", ) parser.add_argument( '--project-id', - metavar="", - help="Uuid of a project object in Keystone", + metavar="", + help="ID of a project object in Keystone", ) parser.add_argument( '--resource-type', - metavar="", + metavar="", choices=['network', 'subnet', 'port', 'router', 'security_group'], help="Available resource types", ) parser.add_argument( "routing", - metavar="", + metavar="", help="ID of the routing resource to update", ) return parser