Support pagination and filtering for job list operations

1. What is the problem
Tricircle job list operations support pagination and
filtering now, which tricircleclient is lacking of.

2. What is the solution for the problem
Support pagination and filtering for job list operation.

3. What the features need to be implemented to the
tricircleclient to realize the solution
Add pagination and filtering for job list operations.

Change-Id: I109f4d5b222472223f3030b9c4eb2fb5503b3d72
This commit is contained in:
Dongfeng Huang 2017-08-28 19:22:19 +08:00
parent 0bc8efd735
commit dc4ef15c2c
9 changed files with 257 additions and 100 deletions

View File

@ -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'}

View File

@ -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)

View File

@ -126,8 +126,8 @@ class TestShowPod(_TestPodCommand, utils.TestCommandWithoutOptions):
class TestListPod(_TestPodCommand):
columns = [
'Id',
'Region name',
'ID',
'Region Name',
]
def setUp(self):

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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="<num-jobs>", type=int,
help="Maximum number of jobs to return",
default=None)
def _add_marker_argument(parser):
parser.add_argument(
'--marker',
dest='marker', metavar="<job>", 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="<project-id>", type=str,
help="ID of a project object in Keystone",
default=None)
parser.add_argument(
'--type',
dest='type', metavar="<type>", type=str,
choices=constants.job_resource_map.keys(),
help="Job type",
default=None)
parser.add_argument(
'--status',
dest='status', metavar="<status>", 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="<project_id>",
metavar="<project-id>",
required=True,
help="Uuid of a project object in Keystone",
help="ID of a project object in Keystone",
)
parser.add_argument(
'--router_id',
metavar="<router_id>",
help="Uuid of a router",
metavar="<router-id>",
help="ID of a router",
)
parser.add_argument(
'--network_id',
metavar="<network_id>",
help="Uuid of a network",
metavar="<network-id>",
help="ID of a network",
)
parser.add_argument(
'--pod_id',
metavar="<pod_id>",
help="Uuid of a pod",
metavar="<pod-id>",
help="ID of a pod",
)
parser.add_argument(
'--port_id',
metavar="<port_id>",
help="Uuid of a port",
metavar="<port-id>",
help="ID of a port",
)
parser.add_argument(
'--trunk_id',
metavar="<trunk_id>",
help="Uuid of a trunk",
metavar="<trunk-id>",
help="ID of a trunk",
)
parser.add_argument(
'--subnet_id',
metavar="<subnet_id>",
help="Uuid of a subnet",
metavar="<subnet-id>",
help="ID of a subnet",
)
parser.add_argument(
'--portchain_id',
metavar="<portchain_id>",
help="Uuid of a port chain",
metavar="<portchain-id>",
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="<id>",
help="Uuid of the job to display",
"job",
metavar="<job>",
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="<id>",
help="Uuid of the job to delete",
"job",
metavar="<job>",
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="<id>",
help="Uuid of the job to redo",
'job',
metavar="<job>",
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)

View File

@ -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'}

View File

@ -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="<num-routings>", 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="<routing>", 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="<routing>", 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="<top-id>", 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="<bottom-id>", 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="<pod-id>", 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="<project-id>", 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="<resource-type>", 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="<created-at>", 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="<updated-at>", 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="<top_id>",
metavar="<top-id>",
required=True,
help="Resource id on Central Neutron",
)
parser.add_argument(
'--bottom-id',
metavar="<bottom_id>",
metavar="<bottom-id>",
required=True,
help="Resource id on Local Neutron",
)
parser.add_argument(
'--pod-id',
metavar="<pod_id>",
metavar="<pod-id>",
required=True,
help="Uuid of a pod",
help="ID of a pod",
)
parser.add_argument(
'--project-id',
metavar="<project_id>",
metavar="<project-id>",
required=True,
help="Uuid of a project object in Keystone",
help="ID of a project object in Keystone",
)
parser.add_argument(
'--resource-type',
metavar="<resource_type>",
metavar="<resource-type>",
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="<routing>",
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="<Routing>",
metavar="<routing>",
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="<top_id>",
metavar="<top-id>",
help="Resource id on Central Neutron",
)
parser.add_argument(
'--bottom-id',
metavar="<bottom_id>",
metavar="<bottom-id>",
help="Resource id on Local Neutron",
)
parser.add_argument(
'--pod-id',
metavar="<pod_id>",
help="Uuid of a pod",
metavar="<pod-id>",
help="ID of a pod",
)
parser.add_argument(
'--project-id',
metavar="<project_id>",
help="Uuid of a project object in Keystone",
metavar="<project-id>",
help="ID of a project object in Keystone",
)
parser.add_argument(
'--resource-type',
metavar="<resource_type>",
metavar="<resource-type>",
choices=['network', 'subnet', 'port', 'router', 'security_group'],
help="Available resource types",
)
parser.add_argument(
"routing",
metavar="<Routing>",
metavar="<routing>",
help="ID of the routing resource to update",
)
return parser