Provide consistent options and return latest execution entries

This patchset updates the *-list commands to have consistent
definitions of the sort_keys/sort_dirs/limit/filter/marker
options.  It also modified the execution-list, action-execution-list
and task-list commands to return the MOST RECENT entries by
default, when no other sort_key, sort_dir or marker options are
provided, rather than the oldest entries.  There is a new --oldest
option for these three commands to allow the user to access the
oldest entries instead of the newest.

A release note has also been created.

Change-Id: I002edd1b10ab281072cfa7501cfa763073a7781c
This commit is contained in:
Bob Haddleton 2018-03-21 10:34:46 -05:00 committed by Bob.Haddleton
parent 7a1c8cc240
commit d53da3629f
24 changed files with 686 additions and 649 deletions

View File

@ -14,9 +14,12 @@
import copy
import json
import six
from keystoneauth1 import exceptions
urlparse = six.moves.urllib.parse
class Resource(object):
resource_name = 'Something'
@ -71,7 +74,41 @@ class ResourceManager(object):
self.http_client = http_client
def find(self, **kwargs):
return [i for i in self.list() if _check_items(i, kwargs.items())]
return [i for i in self._list() if _check_items(i, kwargs.items())]
@staticmethod
def _build_query_params(marker=None, limit=None, sort_keys=None,
sort_dirs=None, fields=None, filters=None,
scope=None, namespace=None):
qparams = {}
if marker:
qparams['marker'] = marker
if limit and limit > 0:
qparams['limit'] = limit
if sort_keys:
qparams['sort_keys'] = sort_keys
if sort_dirs:
qparams['sort_dirs'] = sort_dirs
if fields:
qparams['fields'] = ",".join(fields)
if filters:
for name, val in filters.items():
qparams[name] = val
if scope:
qparams['scope'] = scope
if namespace:
qparams['namespace'] = namespace
return ("?%s" % urlparse.urlencode(list(qparams.items()))
if qparams else "")
def _ensure_not_empty(self, **kwargs):
for name, value in kwargs.items():

View File

@ -13,12 +13,9 @@
# limitations under the License.
import json
import six
from mistralclient.api import base
urlparse = six.moves.urllib.parse
class ActionExecution(base.Resource):
resource_name = 'ActionExecution'
@ -62,7 +59,8 @@ class ActionExecutionManager(base.ResourceManager):
return self._update('/action_executions/%s' % id, data)
def list(self, task_execution_id=None, limit=None):
def list(self, task_execution_id=None, limit=None, marker='', fields=None,
sort_keys='', sort_dirs='', **filters):
url = '/action_executions'
if task_execution_id:
@ -70,13 +68,14 @@ class ActionExecutionManager(base.ResourceManager):
url += "%s"
qparams = {}
if limit and limit > 0:
qparams['limit'] = limit
query_string = ("?%s" % urlparse.urlencode(list(qparams.items()))
if qparams else "")
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list(url % query_string, response_key='action_executions')

View File

@ -12,14 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import six
from keystoneauth1 import exceptions
from mistralclient.api import base
from mistralclient import utils
urlparse = six.moves.urllib.parse
class Action(base.Resource):
resource_name = 'Action'
@ -75,26 +71,16 @@ class ActionManager(base.ResourceManager):
for resource_data in base.extract_json(resp, 'actions')]
def list(self, marker='', limit=None, sort_keys='', sort_dirs='',
**filters):
qparams = {}
fields='', **filters):
if marker:
qparams['marker'] = marker
if limit and limit > 0:
qparams['limit'] = limit
if sort_keys:
qparams['sort_keys'] = sort_keys
if sort_dirs:
qparams['sort_dirs'] = sort_dirs
for name, val in filters.items():
qparams[name] = val
query_string = ("?%s" % urlparse.urlencode(list(qparams.items()))
if qparams else "")
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list(
'/actions%s' % query_string,

View File

@ -54,8 +54,19 @@ class CronTriggerManager(base.ResourceManager):
return self._create('/cron_triggers', data)
def list(self):
return self._list('/cron_triggers', response_key='cron_triggers')
def list(self, marker='', limit=None, sort_keys='', fields='',
sort_dirs='', **filters):
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list('/cron_triggers%s' % query_string,
response_key='cron_triggers')
def get(self, name):
self._ensure_not_empty(name=name)

View File

@ -13,7 +13,6 @@
# limitations under the License.
import json
import six
from mistralclient.api import base
@ -71,8 +70,19 @@ class EnvironmentManager(base.ResourceManager):
return self._update('/environments', kwargs)
def list(self):
return self._list('/environments', response_key='environments')
def list(self, marker='', limit=None, sort_keys='', sort_dirs='',
fields='', **filters):
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list('/environments%s' % query_string,
response_key='environments')
def get(self, name):
self._ensure_not_empty(name=name)

View File

@ -47,8 +47,19 @@ class EventTriggerManager(base.ResourceManager):
return self._create('/event_triggers', data)
def list(self):
return self._list('/event_triggers', response_key='event_triggers')
def list(self, marker='', limit=None, sort_keys='', sort_dirs='',
fields='', **filters):
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list('/event_triggers%s' % query_string,
response_key='event_triggers')
def get(self, id):
self._ensure_not_empty(id=id)

View File

@ -14,16 +14,13 @@
# limitations under the License.
import json
from oslo_utils import uuidutils
import six
from oslo_utils import uuidutils
from mistralclient.api import base
urlparse = six.moves.urllib.parse
class Execution(base.Resource):
resource_name = 'Execution'
@ -79,29 +76,18 @@ class ExecutionManager(base.ResourceManager):
return self._update('/executions/%s' % id, data)
def list(self, task=None, marker='', limit=None, sort_keys='',
sort_dirs='', **filters):
qparams = {}
sort_dirs='', fields='', **filters):
if task:
qparams['task_execution_id'] = task
filters['task_execution_id'] = task
if marker:
qparams['marker'] = marker
if limit and limit > 0:
qparams['limit'] = limit
if sort_keys:
qparams['sort_keys'] = sort_keys
if sort_dirs:
qparams['sort_dirs'] = sort_dirs
for name, val in filters.items():
qparams[name] = val
query_string = ("?%s" % urlparse.urlencode(list(qparams.items()))
if qparams else "")
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list(
'/executions%s' % query_string,
@ -116,11 +102,9 @@ class ExecutionManager(base.ResourceManager):
def delete(self, id, force=None):
self._ensure_not_empty(id=id)
qparams = {}
if force:
qparams['force'] = True
query_string = ("?%s" % urlparse.urlencode(list(qparams.items()))
if qparams else "")
query_string = self._build_query_params(filters=qparams)
self._delete('/executions/%s%s' % (id, query_string))

View File

@ -14,12 +14,9 @@
# limitations under the License.
import json
import six
from mistralclient.api import base
urlparse = six.moves.urllib.parse
class Task(base.Resource):
resource_name = 'Task'
@ -29,7 +26,7 @@ class TaskManager(base.ResourceManager):
resource_class = Task
def list(self, workflow_execution_id=None, marker='', limit=None,
sort_keys='', sort_dirs='', fields=[], **filters):
sort_keys='', sort_dirs='', fields=None, **filters):
url = '/tasks'
if workflow_execution_id:
@ -37,28 +34,14 @@ class TaskManager(base.ResourceManager):
url += '%s'
qparams = {}
if marker:
qparams['marker'] = marker
if limit and limit > 0:
qparams['limit'] = limit
if sort_keys:
qparams['sort_keys'] = sort_keys
if sort_dirs:
qparams['sort_dirs'] = sort_dirs
if fields:
qparams['fields'] = ",".join(fields)
for name, val in filters.items():
qparams[name] = val
query_string = ("?%s" % urlparse.urlencode(list(qparams.items()))
if qparams else "")
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list(url % query_string, response_key='tasks')

View File

@ -82,12 +82,20 @@ class WorkbookManager(base.ResourceManager):
return self.resource_class(self, base.extract_json(resp, None))
def list(self, namespace=''):
return self._list(
self._get_workbooks_url(None, namespace),
response_key='workbooks'
def list(self, namespace='', marker='', limit=None, sort_keys='',
sort_dirs='', fields='', **filters):
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
filters=filters,
namespace=namespace
)
return self._list('/workbooks{}'.format(query_string),
response_key='workbooks')
def get(self, name, namespace=''):
self._ensure_not_empty(name=name)

View File

@ -13,16 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import six
from keystoneauth1 import exceptions
from mistralclient.api import base
from mistralclient import utils
urlparse = six.moves.urllib.parse
class Workflow(base.Resource):
resource_name = 'Workflow'
@ -80,29 +75,18 @@ class WorkflowManager(base.ResourceManager):
for resource_data in base.extract_json(resp, 'workflows')]
def list(self, namespace='', marker='', limit=None, sort_keys='',
sort_dirs='', **filters):
qparams = {}
sort_dirs='', fields='', **filters):
if namespace:
qparams['namespace'] = namespace
filters['namespace'] = namespace
if marker:
qparams['marker'] = marker
if limit and limit > 0:
qparams['limit'] = limit
if sort_keys:
qparams['sort_keys'] = sort_keys
if sort_dirs:
qparams['sort_dirs'] = sort_dirs
for name, val in filters.items():
qparams[name] = val
query_string = ("?%s" % urlparse.urlencode(list(qparams.items()))
if qparams else "")
query_string = self._build_query_params(
marker=marker,
limit=limit,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
fields=fields,
filters=filters
)
return self._list(
'/workflows%s' % query_string,

View File

@ -25,72 +25,54 @@ from mistralclient import utils
LOG = logging.getLogger(__name__)
def format_list(action_ex=None):
columns = (
'ID',
'Name',
'Workflow name',
'Workflow namespace',
'Task name',
'Task ID',
'State',
'Accepted',
'Created at',
'Updated at'
)
class ActionExecutionFormatter(base.MistralFormatter):
COLUMNS = [
('id', 'ID'),
('name', 'Name'),
('workflow_name', 'Workflow name'),
('workflow_namespace', 'Workflow namespace'),
('task_name', 'Task name'),
('task_execution_id', 'Task ID'),
('state', 'State'),
('state_info', 'State info'),
('accepted', 'Accepted'),
('created_at', 'Created at'),
('updated_at', 'Updated at'),
]
if action_ex:
data = (
action_ex.id,
action_ex.name,
action_ex.workflow_name,
action_ex.workflow_namespace,
action_ex.task_name if hasattr(action_ex, 'task_name') else None,
action_ex.task_execution_id,
action_ex.state,
action_ex.accepted,
action_ex.created_at,
action_ex.updated_at or '<none>'
)
else:
data = (tuple('' for _ in range(len(columns))),)
LIST_COLUMN_FIELD_NAMES = [c[0] for c in COLUMNS if c[0] != 'state_info']
LIST_COLUMN_HEADING_NAMES = [c[1] for c in COLUMNS if c[0] != 'state_info']
return columns, data
@staticmethod
def format(action_ex=None, lister=False):
if lister:
columns = ActionExecutionFormatter.LIST_COLUMN_HEADING_NAMES
else:
columns = ActionExecutionFormatter.headings()
if action_ex:
if hasattr(action_ex, 'task_name'):
task_name = action_ex.task_name
else:
task_name = None
data = (
action_ex.id,
action_ex.name,
action_ex.workflow_name,
action_ex.workflow_namespace,
task_name,
action_ex.task_execution_id,
action_ex.state,)
if not lister:
data += (action_ex.state_info,)
data += (
action_ex.accepted,
action_ex.created_at,
action_ex.updated_at or '<none>'
)
else:
data = (tuple('' for _ in range(len(columns))),)
def format(action_ex=None):
columns = (
'ID',
'Name',
'Workflow name',
'Workflow namespace',
'Task name',
'Task ID',
'State',
'State info',
'Accepted',
'Created at',
'Updated at',
)
if action_ex:
data = (
action_ex.id,
action_ex.name,
action_ex.workflow_name,
action_ex.workflow_namespace,
action_ex.task_name if hasattr(action_ex, 'task_name') else None,
action_ex.task_execution_id,
action_ex.state,
action_ex.state_info,
action_ex.accepted,
action_ex.created_at,
action_ex.updated_at or '<none>'
)
else:
data = (tuple('' for _ in range(len(columns))),)
return columns, data
return columns, data
class Create(command.ShowOne):
@ -166,18 +148,18 @@ class Create(command.ShowOne):
)
if not parsed_args.run_sync and parsed_args.save_result:
return format(action_ex)
return ActionExecutionFormatter.format(action_ex)
else:
self.app.stdout.write("%s\n" % action_ex.output)
return None, None
class List(base.MistralLister):
class List(base.MistralExecutionLister):
"""List all Action executions."""
def _get_format_function(self):
return format_list
return ActionExecutionFormatter.format_list
def get_parser(self, prog_name):
parser = super(List, self).get_parser(prog_name)
@ -187,33 +169,21 @@ class List(base.MistralLister):
nargs='?',
help='Task execution ID.'
)
parser.add_argument(
'--limit',
type=int,
help='Maximum number of action-executions to return in a single '
'result. limit is set to %s by default. Use --limit -1 to '
'fetch the full result set.' % base.DEFAULT_LIMIT,
nargs='?'
)
return parser
def _get_resources(self, parsed_args):
if parsed_args.limit is None:
parsed_args.limit = base.DEFAULT_LIMIT
LOG.info(
"limit is set to %s by default. Set "
"the limit explicitly using \'--limit\', if required. "
"Use \'--limit\' -1 to fetch the full result set.",
base.DEFAULT_LIMIT
)
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.action_executions.list(
parsed_args.task_execution_id,
marker=parsed_args.marker,
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
# TODO(bobh) - Uncomment when the fix for bug 1800322 merges
# fields=ActionExecutionFormatter.LIST_COLUMN_FIELD_NAMES,
**base.get_filters(parsed_args)
)
@ -234,7 +204,7 @@ class Get(command.ShowOne):
parsed_args.action_execution
)
return format(execution)
return ActionExecutionFormatter.format(execution)
class Update(command.ShowOne):
@ -270,7 +240,7 @@ class Update(command.ShowOne):
output
)
return format(execution)
return ActionExecutionFormatter.format(execution)
class GetOutput(command.Command):

View File

@ -22,70 +22,66 @@ from mistralclient.commands.v2 import base
from mistralclient import utils
def format_list(action=None):
return format(action, lister=True)
class ActionFormatter(base.MistralFormatter):
COLUMNS = [
('id', 'ID'),
('name', 'Name'),
('is_system', 'Is system'),
('input', 'Input'),
('description', 'Description'),
('tags', 'Tags'),
('created_at', 'Created at'),
('updated_at', 'Updated at')
]
@staticmethod
def format(action=None, lister=False):
if action:
tags = getattr(action, 'tags', None) or []
input_ = action.input if not lister else base.cut(action.input)
desc = (action.description if not lister
else base.cut(action.description))
def format(action=None, lister=False):
columns = (
'ID',
'Name',
'Is system',
'Input',
'Description',
'Tags',
'Created at',
'Updated at'
)
data = (
action.id,
action.name,
action.is_system,
input_,
desc,
base.wrap(', '.join(tags)) or '<none>',
action.created_at,
)
if hasattr(action, 'updated_at'):
data += (action.updated_at,)
else:
data += (None,)
if action:
tags = getattr(action, 'tags', None) or []
input = action.input if not lister else base.cut(action.input)
desc = (action.description if not lister
else base.cut(action.description))
data = (
action.id,
action.name,
action.is_system,
input,
desc,
base.wrap(', '.join(tags)) or '<none>',
action.created_at,
)
if hasattr(action, 'updated_at'):
data += (action.updated_at,)
else:
data += (None,)
else:
data = (tuple('' for _ in range(len(columns))),)
data = (tuple('' for _ in range(len(ActionFormatter.COLUMNS))),)
return columns, data
return ActionFormatter.headings(), data
class List(base.MistralLister):
"""List all actions."""
def _get_format_function(self):
return format_list
return ActionFormatter.format_list
def get_parser(self, prog_name):
parser = super(List, self).get_parser(prog_name)
parser.add_argument(
'--filter',
dest='filters',
action='append',
help='Filters. Can be repeated.'
)
return parser
def _get_resources(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.actions.list(
marker=parsed_args.marker,
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=ActionFormatter.fields(),
**base.get_filters(parsed_args)
)
@ -104,7 +100,7 @@ class Get(command.ShowOne):
mistral_client = self.app.client_manager.workflow_engine
action = mistral_client.actions.get(parsed_args.action)
return format(action)
return ActionFormatter.format(action)
class Create(base.MistralLister):
@ -131,7 +127,7 @@ class Create(base.MistralLister):
raise RuntimeError("Provide action definition file.")
def _get_format_function(self):
return format_list
return ActionFormatter.format_list
def _get_resources(self, parsed_args):
scope = 'public' if parsed_args.public else 'private'
@ -190,7 +186,7 @@ class Update(base.MistralLister):
return parser
def _get_format_function(self):
return format_list
return ActionFormatter.format_list
def _get_resources(self, parsed_args):
scope = 'public' if parsed_args.public else 'private'
@ -223,8 +219,8 @@ class GetDefinition(command.Command):
class Validate(command.ShowOne):
"""Validate action."""
def _format(self, result=None):
@staticmethod
def _format(result=None):
columns = ('Valid', 'Error')
if result:

View File

@ -24,12 +24,75 @@ import six
DEFAULT_LIMIT = 100
@six.add_metaclass(abc.ABCMeta)
class MistralFormatter(object):
COLUMNS = []
@classmethod
def fields(cls):
return [c[0] for c in cls.COLUMNS]
@classmethod
def headings(cls):
return [c[1] for c in cls.COLUMNS]
@classmethod
def format_list(cls, instance=None):
return cls.format(instance, lister=True)
@staticmethod
def format(instance=None, lister=False):
pass
@six.add_metaclass(abc.ABCMeta)
class MistralLister(command.Lister):
@abc.abstractmethod
def _get_format_function(self):
raise NotImplementedError
def get_parser(self, parsed_args):
parser = super(MistralLister, self).get_parser(parsed_args)
parser.add_argument(
'--marker',
type=str,
help='The last execution uuid of the previous page, displays list '
'of executions after "marker".',
default='',
nargs='?'
)
parser.add_argument(
'--limit',
type=int,
help='Maximum number of entries to return in a single result. ',
nargs='?'
)
parser.add_argument(
'--sort_keys',
help='Comma-separated list of sort keys to sort results by. '
'Default: created_at. '
'Example: mistral execution-list --sort_keys=id,description',
default='created_at',
nargs='?'
)
parser.add_argument(
'--sort_dirs',
help='Comma-separated list of sort directions. Default: asc. '
'Example: mistral execution-list --sort_keys=id,description '
'--sort_dirs=asc,desc',
default='asc',
nargs='?'
)
parser.add_argument(
'--filter',
dest='filters',
action='append',
help='Filters. Can be repeated.'
)
return parser
@abc.abstractmethod
def _get_resources(self, parsed_args):
"""Gets a list of API resources (e.g. using client)."""
@ -56,6 +119,48 @@ class MistralLister(command.Lister):
return f()
@six.add_metaclass(abc.ABCMeta)
class MistralExecutionLister(MistralLister):
def get_parser(self, parsed_args):
parser = super(MistralExecutionLister, self).get_parser(parsed_args)
parser.set_defaults(limit=DEFAULT_LIMIT)
parser.add_argument(
'--oldest',
help='Display the executions starting from the oldest entries '
'instead of the newest',
default=False,
action='store_true'
)
return parser
def take_action(self, parsed_args):
self._validate_parsed_args(parsed_args)
f = self._get_format_function()
reverse_results = False
if (parsed_args.marker == '' and parsed_args.sort_dirs == 'asc' and
parsed_args.sort_keys == 'created_at' and
not parsed_args.oldest):
reverse_results = True
parsed_args.sort_dirs = 'desc'
ret = self._get_resources(parsed_args)
if not isinstance(ret, list):
ret = [ret]
if reverse_results:
ret.reverse()
data = [f(r)[1] for r in ret]
if data:
return f()[0], data
else:
return f()
def cut(string, length=25):
if string and len(string) > length:
return "%s..." % string[:length]

View File

@ -22,60 +22,67 @@ from mistralclient.commands.v2 import base
from mistralclient import utils
def format_list(trigger=None):
return format(trigger, lister=True)
def format(trigger=None, lister=False):
columns = (
'Name',
'Workflow',
'Params',
'Pattern',
class CronTriggerFormatter(base.MistralFormatter):
COLUMNS = [
('name', 'Name'),
('workflow_name', 'Workflow'),
('workflow_params', 'Params'),
('pattern', 'Pattern'),
# TODO(rakhmerov): Uncomment when passwords are handled properly.
# TODO(rakhmerov): Add 'Workflow input' column.
'Next execution time',
'Remaining executions',
'Created at',
'Updated at'
)
('next_execution_time', 'Next execution time'),
('remaining_executions', 'Remaining executions'),
('created_at', 'Created at'),
('updated_at', 'Updated at')
]
if trigger:
# TODO(rakhmerov): Add following here:
# TODO(rakhmerov): wf_input = trigger.workflow_input if not lister
# TODO(rakhmerov:): else base.cut(trigger.workflow_input)
@staticmethod
def format(trigger=None, lister=False):
if trigger:
# TODO(rakhmerov): Add following here:
# TODO(rakhmerov): wf_input = trigger.workflow_input if not lister
# TODO(rakhmerov:): else base.cut(trigger.workflow_input)
data = (
trigger.name,
trigger.workflow_name,
trigger.workflow_params,
trigger.pattern,
# TODO(rakhmerov): Uncomment when passwords are handled properly.
# TODo(rakhmerov): Add 'wf_input' here.
trigger.next_execution_time,
trigger.remaining_executions,
trigger.created_at,
)
data = (
trigger.name,
trigger.workflow_name,
trigger.workflow_params,
trigger.pattern,
# TODO(rakhmerov): Uncomment when passwords are handled
# properly.
# TODo(rakhmerov): Add 'wf_input' here.
trigger.next_execution_time,
trigger.remaining_executions,
trigger.created_at,
)
if hasattr(trigger, 'updated_at'):
data += (trigger.updated_at,)
if hasattr(trigger, 'updated_at'):
data += (trigger.updated_at,)
else:
data += (None,)
else:
data += (None,)
else:
data = (tuple('' for _ in range(len(columns))),)
data = (tuple('' for _ in
range(len(CronTriggerFormatter.COLUMNS))),)
return columns, data
return CronTriggerFormatter.headings(), data
class List(base.MistralLister):
"""List all cron triggers."""
def _get_format_function(self):
return format_list
return CronTriggerFormatter.format_list
def _get_resources(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.cron_triggers.list()
return mistral_client.cron_triggers.list(
marker=parsed_args.marker,
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=CronTriggerFormatter.fields(),
**base.get_filters(parsed_args)
)
class Get(command.ShowOne):
@ -91,7 +98,7 @@ class Get(command.ShowOne):
def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return format(mistral_client.cron_triggers.get(
return CronTriggerFormatter.format(mistral_client.cron_triggers.get(
parsed_args.cron_trigger
))
@ -189,7 +196,7 @@ class Create(command.ShowOne):
parsed_args.count
)
return format(trigger)
return CronTriggerFormatter.format(trigger)
class Delete(command.Command):

View File

@ -21,78 +21,63 @@ from mistralclient.commands.v2 import base
from mistralclient import utils
def format_list(environment=None):
columns = (
'Name',
'Description',
'Scope',
'Created at',
'Updated at'
)
class EnvironmentFormatter(base.MistralFormatter):
COLUMNS = [
('name', 'Name'),
('description', 'Description'),
('variables', 'Variables'),
('scope', 'Scope'),
('created_at', 'Created at'),
('updated_at', 'Updated at'),
]
LIST_COLUMN_FIELD_NAMES = [c[0] for c in COLUMNS if c[0] != 'variables']
LIST_COLUMN_HEADING_NAMES = [c[1] for c in COLUMNS if c[0] != 'variables']
if environment:
data = (
environment.name,
environment.description,
environment.scope,
environment.created_at,
)
if hasattr(environment, 'updated_at'):
data += (environment.updated_at or '<none>',)
@staticmethod
def format(environment=None, lister=False):
if lister:
columns = EnvironmentFormatter.LIST_COLUMN_HEADING_NAMES
else:
data += (None,)
columns = EnvironmentFormatter.headings()
else:
data = (tuple('<none>' for _ in range(len(columns))),)
return columns, data
def format(environment=None):
columns = (
'Name',
'Description',
'Variables',
'Scope',
'Created at',
'Updated at'
)
if environment:
data = (environment.name,)
if hasattr(environment, 'description'):
data += (environment.description or '<none>',)
if environment:
data = (
environment.name,)
if hasattr(environment, 'description'):
data += (environment.description or '<none>',)
else:
data += (None,)
if not lister:
data += (json.dumps(environment.variables, indent=4),)
data += (
environment.scope,
environment.created_at,)
if hasattr(environment, 'updated_at'):
data += (environment.updated_at or '<none>',)
else:
data += (None,)
else:
data += (None,)
data = (tuple('' for _ in range(len(columns))),)
data += (
json.dumps(environment.variables, indent=4),
environment.scope,
environment.created_at,
)
if hasattr(environment, 'updated_at'):
data += (environment.updated_at or '<none>',)
else:
data += (None,)
else:
data = (tuple('' for _ in range(len(columns))),)
return columns, data
return columns, data
class List(base.MistralLister):
"""List all environments."""
def _get_format_function(self):
return format_list
return EnvironmentFormatter.format_list
def _get_resources(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.environments.list()
return mistral_client.environments.list(
marker=parsed_args.marker,
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=EnvironmentFormatter.fields(),
**base.get_filters(parsed_args)
)
class Get(command.ShowOne):
@ -132,7 +117,7 @@ class Get(command.ShowOne):
return columns, data
return format(environment)
return EnvironmentFormatter.format(environment)
class Create(command.ShowOne):
@ -155,7 +140,7 @@ class Create(command.ShowOne):
mistral_client = self.app.client_manager.workflow_engine
environment = mistral_client.environments.create(**data)
return format(environment)
return EnvironmentFormatter.format(environment)
class Delete(command.Command):
@ -203,4 +188,4 @@ class Update(command.ShowOne):
mistral_client = self.app.client_manager.workflow_engine
environment = mistral_client.environments.update(**data)
return format(environment)
return EnvironmentFormatter.format(environment)

View File

@ -19,54 +19,60 @@ from mistralclient.commands.v2 import base
from mistralclient import utils
def format_list(trigger=None):
return format(trigger, lister=True)
class EventTriggerFormatter(base.MistralFormatter):
COLUMNS = [
('id', 'ID'),
('name', 'Name'),
('workflow_id', 'Workflow ID'),
('workflow_params', 'Params'),
('exchange', 'Exchange'),
('topic', 'Topic'),
('event', 'Event'),
('created_at', 'Created at'),
('updated_at', 'Updated at')
]
@staticmethod
def format(trigger=None, lister=False):
if trigger:
data = (
trigger.id,
trigger.name,
trigger.workflow_id,
trigger.workflow_params,
trigger.exchange,
trigger.topic,
trigger.event,
trigger.created_at,
)
def format(trigger=None, lister=False):
columns = (
'ID',
'Name',
'Workflow ID',
'Params',
'Exchange',
'Topic',
'Event',
'Created at',
'Updated at'
)
if trigger:
data = (
trigger.id,
trigger.name,
trigger.workflow_id,
trigger.workflow_params,
trigger.exchange,
trigger.topic,
trigger.event,
trigger.created_at,
)
if hasattr(trigger, 'updated_at'):
data += (trigger.updated_at,)
if hasattr(trigger, 'updated_at'):
data += (trigger.updated_at,)
else:
data += (None,)
else:
data += (None,)
else:
data = (tuple('' for _ in range(len(columns))),)
data = (tuple('' for _ in
range(len(EventTriggerFormatter.COLUMNS))),)
return columns, data
return EventTriggerFormatter.headings(), data
class List(base.MistralLister):
"""List all event triggers."""
def _get_format_function(self):
return format_list
return EventTriggerFormatter.format_list
def _get_resources(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.event_triggers.list()
return mistral_client.event_triggers.list(
marker=parsed_args.marker,
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=EventTriggerFormatter.fields(),
**base.get_filters(parsed_args)
)
class Get(command.ShowOne):
@ -82,9 +88,8 @@ class Get(command.ShowOne):
def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return format(mistral_client.event_triggers.get(
parsed_args.event_trigger
))
return EventTriggerFormatter.format(mistral_client.event_triggers.get(
parsed_args.event_trigger))
class Create(command.ShowOne):
@ -140,7 +145,7 @@ class Create(command.ShowOne):
wf_params,
)
return format(trigger)
return EventTriggerFormatter.format(trigger)
class Delete(command.Command):

View File

@ -29,54 +29,54 @@ from mistralclient import utils
LOG = logging.getLogger(__name__)
def format_list(execution=None):
return format(execution, lister=True)
class ExecutionFormatter(base.MistralFormatter):
COLUMNS = [
('id', 'ID'),
('workflow_id', 'Workflow ID'),
('workflow_name', 'Workflow name'),
('workflow_namespace', 'Workflow namespace'),
('description', 'Description'),
('task_execution_id', 'Task Execution ID'),
('root_execution_id', 'Root Execution ID'),
('state', 'State'),
('state_info', 'State info'),
('created_at', 'Created at'),
('updated_at', 'Updated at'),
]
@staticmethod
def format(execution=None, lister=False):
# TODO(nmakhotkin) Add parent task id when it's implemented in API.
if execution:
state_info = (execution.state_info if not lister
else base.cut(execution.state_info))
data = (
execution.id,
execution.workflow_id,
execution.workflow_name,
execution.workflow_namespace,
execution.description,
execution.task_execution_id or '<none>',
execution.root_execution_id or '<none>',
execution.state,
state_info,
execution.created_at,
execution.updated_at or '<none>'
)
else:
data = (tuple('' for _ in
range(len(ExecutionFormatter.COLUMNS))),)
return ExecutionFormatter.headings(), data
def format(execution=None, lister=False):
columns = (
'ID',
'Workflow ID',
'Workflow name',
'Workflow namespace',
'Description',
'Task Execution ID',
'Root Execution ID',
'State',
'State info',
'Created at',
'Updated at'
)
# TODO(nmakhotkin) Add parent task id when it's implemented in API.
if execution:
state_info = (execution.state_info if not lister
else base.cut(execution.state_info))
data = (
execution.id,
execution.workflow_id,
execution.workflow_name,
execution.workflow_namespace,
execution.description,
execution.task_execution_id or '<none>',
execution.root_execution_id or '<none>',
execution.state,
state_info,
execution.created_at,
execution.updated_at or '<none>'
)
else:
data = (tuple('' for _ in range(len(columns))),)
return columns, data
class List(base.MistralLister):
class List(base.MistralExecutionLister):
"""List all executions."""
def _get_format_function(self):
return format_list
return ExecutionFormatter.format_list
def get_parser(self, parsed_args):
parser = super(List, self).get_parser(parsed_args)
@ -86,58 +86,10 @@ class List(base.MistralLister):
help="Parent task execution ID associated with workflow "
"execution list.",
)
parser.add_argument(
'--marker',
type=str,
help='The last execution uuid of the previous page, displays list '
'of executions after "marker".',
default='',
nargs='?'
)
parser.add_argument(
'--limit',
type=int,
help='Maximum number of executions to return in a single result. '
'limit is set to %s by default. Use --limit -1 to fetch the '
'full result set.' % base.DEFAULT_LIMIT,
nargs='?'
)
parser.add_argument(
'--sort_keys',
help='Comma-separated list of sort keys to sort results by. '
'Default: created_at. '
'Example: mistral execution-list --sort_keys=id,description',
default='created_at',
nargs='?'
)
parser.add_argument(
'--sort_dirs',
help='Comma-separated list of sort directions. Default: asc. '
'Example: mistral execution-list --sort_keys=id,description '
'--sort_dirs=asc,desc',
default='asc',
nargs='?'
)
parser.add_argument(
'--filter',
dest='filters',
action='append',
help='Filters. Can be repeated.'
)
return parser
def _get_resources(self, parsed_args):
if parsed_args.limit is None:
parsed_args.limit = base.DEFAULT_LIMIT
LOG.info(
"limit is set to %s by default. Set "
"the limit explicitly using \'--limit\', if required. "
"Use \'--limit\' -1 to fetch the full result set.",
base.DEFAULT_LIMIT
)
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.executions.list(
@ -146,6 +98,7 @@ class List(base.MistralLister):
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=ExecutionFormatter.fields(),
**base.get_filters(parsed_args)
)
@ -164,7 +117,7 @@ class Get(command.ShowOne):
mistral_client = self.app.client_manager.workflow_engine
execution = mistral_client.executions.get(parsed_args.execution)
return format(execution)
return ExecutionFormatter.format(execution)
class Create(command.ShowOne):
@ -235,7 +188,7 @@ class Create(command.ShowOne):
**params
)
return format(execution)
return ExecutionFormatter.format(execution)
class Delete(command.Command):
@ -322,7 +275,7 @@ class Update(command.ShowOne):
env=env
)
return format(execution)
return ExecutionFormatter.format(execution)
class GetInput(command.Command):

View File

@ -19,46 +19,44 @@ from mistralclient.commands.v2 import base
from mistralclient import exceptions
def format_list(member=None):
return format(member, lister=True)
class MemberFormatter(base.MistralFormatter):
COLUMNS = [
('resource_id', 'Resource ID'),
('resource_type', 'Resource Type'),
('project_id', 'Resource Owner'),
('member_id', 'Member ID'),
('status', 'Status'),
('created_at', 'Created at'),
('updated_at', 'Updated at')
]
@staticmethod
def format(member=None, lister=False):
if member:
data = (
member.resource_id,
member.resource_type,
member.project_id,
member.member_id,
member.status,
member.created_at,
)
def format(member=None, lister=False):
columns = (
'Resource ID',
'Resource Type',
'Resource Owner',
'Member ID',
'Status',
'Created at',
'Updated at'
)
if member:
data = (
member.resource_id,
member.resource_type,
member.project_id,
member.member_id,
member.status,
member.created_at,
)
if hasattr(member, 'updated_at'):
data += (member.updated_at,)
if hasattr(member, 'updated_at'):
data += (member.updated_at,)
else:
data += (None,)
else:
data += (None,)
else:
data = (tuple('' for _ in range(len(columns))),)
data = (tuple('' for _ in range(len(MemberFormatter.COLUMNS))),)
return columns, data
return MemberFormatter.headings(), data
class List(base.MistralLister):
"""List all members."""
def _get_format_function(self):
return format_list
return MemberFormatter.format_list
def get_parser(self, parsed_args):
parser = super(List, self).get_parser(parsed_args)
@ -114,7 +112,7 @@ class Get(command.ShowOne):
parsed_args.member_id,
)
return format(member)
return MemberFormatter.format(member)
class Create(command.ShowOne):
@ -146,7 +144,7 @@ class Create(command.ShowOne):
parsed_args.member_id,
)
return format(member)
return MemberFormatter.format(member)
class Delete(command.Command):
@ -232,4 +230,4 @@ class Update(command.ShowOne):
status=parsed_args.status
)
return format(member)
return MemberFormatter.format(member)

View File

@ -27,7 +27,7 @@ from mistralclient import utils
LOG = logging.getLogger(__name__)
class TaskFormatter(object):
class TaskFormatter(base.MistralFormatter):
COLUMNS = [
('id', 'ID'),
('name', 'Name'),
@ -40,13 +40,6 @@ class TaskFormatter(object):
('updated_at', 'Updated at'),
]
COLUMN_FIELD_NAMES = list(zip(*COLUMNS))[0]
COLUMN_HEADING_NAMES = list(zip(*COLUMNS))[1]
@staticmethod
def format_list(task=None):
return TaskFormatter.format(task, lister=True)
@staticmethod
def format(task=None, lister=False):
if task:
@ -67,10 +60,10 @@ class TaskFormatter(object):
else:
data = (tuple('' for _ in range(len(TaskFormatter.COLUMNS))),)
return TaskFormatter.COLUMN_HEADING_NAMES, data
return TaskFormatter.headings(), data
class List(base.MistralLister):
class List(base.MistralExecutionLister):
"""List all tasks."""
def get_parser(self, prog_name):
@ -81,43 +74,21 @@ class List(base.MistralLister):
nargs='?',
help='Workflow execution ID associated with list of Tasks.'
)
parser.add_argument(
'--filter',
dest='filters',
action='append',
help='Filters. Can be repeated.'
)
parser.add_argument(
'--limit',
type=int,
help='Maximum number of tasks to return in a single result. '
'limit is set to %s by default. Use --limit -1 to fetch the '
'full result set.' % base.DEFAULT_LIMIT,
nargs='?'
)
return parser
def _get_format_function(self):
return TaskFormatter.format_list
def _get_resources(self, parsed_args):
if parsed_args.limit is None:
parsed_args.limit = base.DEFAULT_LIMIT
LOG.info(
"limit is set to %s by default. Set "
"the limit explicitly using \'--limit\', if required. "
"Use \'--limit\' -1 to fetch the full result set.",
base.DEFAULT_LIMIT
)
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.tasks.list(
parsed_args.workflow_execution,
marker=parsed_args.marker,
limit=parsed_args.limit,
fields=TaskFormatter.COLUMN_FIELD_NAMES,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=TaskFormatter.fields(),
**base.get_filters(parsed_args)
)

View File

@ -21,45 +21,54 @@ from mistralclient.commands.v2 import base
from mistralclient import utils
def format(workbook=None):
columns = (
'Name',
'Namespace',
'Tags',
'Scope',
'Created at',
'Updated at'
)
class WorkbookFormatter(base.MistralFormatter):
COLUMNS = [
('name', 'Name'),
('namespace', 'Namespace'),
('tags', 'Tags'),
('scope', 'Scope'),
('created_at', 'Created at'),
('updated_at', 'Updated at')
]
if workbook:
data = (
workbook.name,
workbook.namespace,
base.wrap(', '.join(workbook.tags or '')) or '<none>',
workbook.scope,
workbook.created_at,
)
@staticmethod
def format(workbook=None, lister=False):
if workbook:
data = (
workbook.name,
workbook.namespace,
base.wrap(', '.join(workbook.tags or '')) or '<none>',
workbook.scope,
workbook.created_at,
)
if hasattr(workbook, 'updated_at'):
data += (workbook.updated_at,)
else:
data += (None,)
if hasattr(workbook, 'updated_at'):
data += (workbook.updated_at,)
else:
data += (None,)
data = (tuple('' for _ in range(len(WorkbookFormatter.COLUMNS))),)
else:
data = (tuple('' for _ in range(len(columns))),)
return columns, data
return WorkbookFormatter.headings(), data
class List(base.MistralLister):
"""List all workbooks."""
def _get_format_function(self):
return format
return WorkbookFormatter.format
def _get_resources(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.workbooks.list()
return mistral_client.workbooks.list(
marker=parsed_args.marker,
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=WorkbookFormatter.fields(),
**base.get_filters(parsed_args)
)
class Get(command.ShowOne):
@ -86,7 +95,7 @@ class Get(command.ShowOne):
parsed_args.namespace
)
return format(workbook)
return WorkbookFormatter.format(workbook)
class Create(command.ShowOne):
@ -125,7 +134,7 @@ class Create(command.ShowOne):
scope=scope
)
return format(workbook)
return WorkbookFormatter.format(workbook)
class Delete(command.Command):
@ -190,7 +199,7 @@ class Update(command.ShowOne):
scope=scope
)
return format(workbook)
return WorkbookFormatter.format(workbook)
class GetDefinition(command.Command):
@ -212,8 +221,8 @@ class GetDefinition(command.Command):
class Validate(command.ShowOne):
"""Validate workbook."""
def _format(self, result=None):
@staticmethod
def _format(result=None):
columns = ('Valid', 'Error')
if result:

View File

@ -22,69 +22,65 @@ from mistralclient.commands.v2 import base
from mistralclient import utils
def format_list(workflow=None):
return format(workflow, lister=True)
class WorkflowFormatter(base.MistralFormatter):
COLUMNS = [
('id', 'ID'),
('name', 'Name'),
('namespace', 'Namespace'),
('project_id', 'Project ID'),
('tags', 'Tags'),
('input', 'Input'),
('scope', 'Scope'),
('created_at', 'Created at'),
('updated_at', 'Updated at')
]
@staticmethod
def format(workflow=None, lister=False):
if workflow:
tags = getattr(workflow, 'tags', None) or []
def format(workflow=None, lister=False):
columns = (
'ID',
'Name',
'Namespace',
'Project ID',
'Tags',
'Input',
'Scope',
'Created at',
'Updated at'
)
data = (
workflow.id,
workflow.name,
workflow.namespace,
workflow.project_id,
base.wrap(', '.join(tags)) or '<none>',
workflow.input if not lister else base.cut(workflow.input),
workflow.scope,
workflow.created_at
)
if workflow:
tags = getattr(workflow, 'tags', None) or []
data = (
workflow.id,
workflow.name,
workflow.namespace,
workflow.project_id,
base.wrap(', '.join(tags)) or '<none>',
workflow.input if not lister else base.cut(workflow.input),
workflow.scope,
workflow.created_at
)
if hasattr(workflow, 'updated_at'):
data += (workflow.updated_at,)
if hasattr(workflow, 'updated_at'):
data += (workflow.updated_at,)
else:
data += (None,)
else:
data += (None,)
else:
data = (tuple('' for _ in range(len(columns))),)
data = (tuple('' for _ in range(len(WorkflowFormatter.COLUMNS))),)
return columns, data
return WorkflowFormatter.headings(), data
class List(base.MistralLister):
"""List all workflows."""
def _get_format_function(self):
return format_list
return WorkflowFormatter.format_list
def get_parser(self, prog_name):
parser = super(List, self).get_parser(prog_name)
parser.add_argument(
'--filter',
dest='filters',
action='append',
help='Filters. Can be repeated.'
)
return parser
def _get_resources(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine
return mistral_client.workflows.list(
marker=parsed_args.marker,
limit=parsed_args.limit,
sort_keys=parsed_args.sort_keys,
sort_dirs=parsed_args.sort_dirs,
fields=WorkflowFormatter.fields(),
**base.get_filters(parsed_args)
)
@ -113,7 +109,7 @@ class Get(show.ShowOne):
parsed_args.namespace
)
return format(wf)
return WorkflowFormatter.format(wf)
class Create(base.MistralLister):
@ -142,7 +138,7 @@ class Create(base.MistralLister):
return parser
def _get_format_function(self):
return format_list
return WorkflowFormatter.format_list
def _validate_parsed_args(self, parsed_args):
if not parsed_args.definition:
@ -223,7 +219,7 @@ class Update(base.MistralLister):
return parser
def _get_format_function(self):
return format_list
return WorkflowFormatter.format_list
def _get_resources(self, parsed_args):
scope = 'public' if parsed_args.public else 'private'
@ -267,8 +263,8 @@ class GetDefinition(command.Command):
class Validate(show.ShowOne):
"""Validate workflow."""
def _format(self, result=None):
@staticmethod
def _format(result=None):
columns = ('Valid', 'Error')
if result:

View File

@ -225,7 +225,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
)
def test_list(self):
self.client.executions.list.return_value = [EXEC, SUB_WF_EXEC]
self.client.executions.list.return_value = [SUB_WF_EXEC, EXEC]
result = self.call(execution_cmd.List)
@ -239,28 +239,46 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
self.call(execution_cmd.List)
self.client.executions.list.assert_called_once_with(
fields=execution_cmd.ExecutionFormatter.fields(),
limit=100,
marker='',
sort_dirs='asc',
sort_dirs='desc',
sort_keys='created_at',
task=None
)
self.call(
execution_cmd.List,
app_args=[
'--oldest'
]
)
self.client.executions.list.assert_called_with(
fields=execution_cmd.ExecutionFormatter.fields(),
limit=100,
marker='',
sort_keys='created_at',
sort_dirs='asc',
task=None
)
self.call(
execution_cmd.List,
app_args=[
'--limit', '5',
'--sort_dirs', 'id, Workflow',
'--sort_keys', 'desc',
'--sort_keys', 'id, Workflow',
'--sort_dirs', 'desc',
'--marker', 'abc'
]
)
self.client.executions.list.assert_called_with(
fields=execution_cmd.ExecutionFormatter.fields(),
limit=5,
marker='abc',
sort_dirs='id, Workflow',
sort_keys='desc',
sort_keys='id, Workflow',
sort_dirs='desc',
task=None
)

View File

@ -61,7 +61,7 @@ class TestCLITasksV2(base.BaseCommandTest):
self.assertEqual([EXPECTED_TASK_RESULT], result[1])
self.assertEqual(
self.client.tasks.list.call_args[1]["fields"],
task_cmd.TaskFormatter.COLUMN_FIELD_NAMES
task_cmd.TaskFormatter.fields()
)
def test_list_with_workflow_execution(self):

View File

@ -0,0 +1,11 @@
---
critical:
- |
The default behavior of the action-execution-list, execution-list and
task-list commands has been changed. Instead of returning the oldest
N records (default 100 or --limit specified value) by default,
they now return the most recent N records, when no other sort_dir,
sort_key or marker values are provided. If the user specifies --oldest
or any of the --marker, --sort_key or --sort_dir options, the new
behavior is disabled and the commands will work according to the
user-supplied options.