Add "duration" to workflow executions printed by CLI commands
* It's convenient to see duration of an execution right away w/o having to calculate it ourselves. * Minor style changes according to the Mistral Coding Guidelines. Change-Id: Ibfe806d1f1fcebb9ca0459c82daded308677de44
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# Copyright 2014 - Mirantis, Inc.
|
# Copyright 2014 - Mirantis, Inc.
|
||||||
# Copyright 2015 - StackStorm, Inc.
|
# Copyright 2015 - StackStorm, Inc.
|
||||||
|
# Copyright 2020 - Nokia Software.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
# Copyright 2014 - Mirantis, Inc.
|
# Copyright 2014 - Mirantis, Inc.
|
||||||
# All Rights Reserved
|
# Copyright 2020 - Nokia Software.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
# not use this file except in compliance with the License. You may obtain
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import datetime as dt
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from osc_lib.command import command
|
from osc_lib.command import command
|
||||||
@@ -30,7 +31,12 @@ class MistralFormatter(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fields(cls):
|
def fields(cls):
|
||||||
return [c[0] for c in cls.COLUMNS]
|
# Column should be a tuple:
|
||||||
|
# (<field name>, <field title>, <optional synthetic flag>)
|
||||||
|
# If the 3rd value is specified and it's True then
|
||||||
|
# the field is synthetic (calculated) and should not be requested
|
||||||
|
# from the API client.
|
||||||
|
return [c[0] for c in cls.COLUMNS if len(c) == 2 or not c[2]]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def headings(cls):
|
def headings(cls):
|
||||||
@@ -188,3 +194,34 @@ def get_filters(parsed_args):
|
|||||||
filters[arr[0]] = arr[1]
|
filters[arr[0]] = arr[1]
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
|
def get_duration_str(start_dt_str, end_dt_str):
|
||||||
|
"""Builds a human friendly duration string.
|
||||||
|
|
||||||
|
:param start_dt_str: Start date time as an ISO string. Must not be empty.
|
||||||
|
:param end_dt_str: End date time as an ISO string. If empty, duration is
|
||||||
|
calculated from the current time.
|
||||||
|
:return: Duration(delta) string.
|
||||||
|
"""
|
||||||
|
start_dt = dt.datetime.strptime(start_dt_str, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
if end_dt_str:
|
||||||
|
end_dt = dt.datetime.strptime(end_dt_str, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
return str(end_dt - start_dt)
|
||||||
|
|
||||||
|
delta_from_now = dt.datetime.utcnow() - start_dt
|
||||||
|
|
||||||
|
# If delta is too small then we won't show any value. It means that
|
||||||
|
# the corresponding process (e.g. an execution) just started.
|
||||||
|
if delta_from_now < dt.timedelta(seconds=2):
|
||||||
|
return '...'
|
||||||
|
|
||||||
|
# Drop microseconds to decrease verbosity.
|
||||||
|
delta = (
|
||||||
|
delta_from_now
|
||||||
|
- dt.timedelta(microseconds=delta_from_now.microseconds)
|
||||||
|
)
|
||||||
|
|
||||||
|
return "{}...".format(delta)
|
||||||
|
@@ -44,28 +44,35 @@ class ExecutionFormatter(base.MistralFormatter):
|
|||||||
('state_info', 'State info'),
|
('state_info', 'State info'),
|
||||||
('created_at', 'Created at'),
|
('created_at', 'Created at'),
|
||||||
('updated_at', 'Updated at'),
|
('updated_at', 'Updated at'),
|
||||||
|
('duration', 'Duration', True),
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format(execution=None, lister=False):
|
def format(wf_ex=None, lister=False):
|
||||||
# TODO(nmakhotkin) Add parent task id when it's implemented in API.
|
if wf_ex:
|
||||||
|
state_info = (
|
||||||
|
wf_ex.state_info if not lister
|
||||||
|
else base.cut(wf_ex.state_info)
|
||||||
|
)
|
||||||
|
|
||||||
if execution:
|
duration = base.get_duration_str(
|
||||||
state_info = (execution.state_info if not lister
|
wf_ex.created_at,
|
||||||
else base.cut(execution.state_info))
|
wf_ex.updated_at if wf_ex.state in ['ERROR', 'SUCCESS'] else ''
|
||||||
|
)
|
||||||
|
|
||||||
data = (
|
data = (
|
||||||
execution.id,
|
wf_ex.id,
|
||||||
execution.workflow_id,
|
wf_ex.workflow_id,
|
||||||
execution.workflow_name,
|
wf_ex.workflow_name,
|
||||||
execution.workflow_namespace,
|
wf_ex.workflow_namespace,
|
||||||
execution.description,
|
wf_ex.description,
|
||||||
execution.task_execution_id or '<none>',
|
wf_ex.task_execution_id or '<none>',
|
||||||
execution.root_execution_id or '<none>',
|
wf_ex.root_execution_id or '<none>',
|
||||||
execution.state,
|
wf_ex.state,
|
||||||
state_info,
|
state_info,
|
||||||
execution.created_at,
|
wf_ex.created_at,
|
||||||
execution.updated_at or '<none>'
|
wf_ex.updated_at or '<none>',
|
||||||
|
duration
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
data = (tuple('' for _ in
|
data = (tuple('' for _ in
|
||||||
|
@@ -22,12 +22,14 @@ class BaseClientTest(base.BaseTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BaseClientTest, self).setUp()
|
super(BaseClientTest, self).setUp()
|
||||||
|
|
||||||
self.requests_mock = self.useFixture(fixture.Fixture())
|
self.requests_mock = self.useFixture(fixture.Fixture())
|
||||||
|
|
||||||
|
|
||||||
class BaseCommandTest(base.BaseTestCase):
|
class BaseCommandTest(base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BaseCommandTest, self).setUp()
|
super(BaseCommandTest, self).setUp()
|
||||||
|
|
||||||
self.app = mock.Mock()
|
self.app = mock.Mock()
|
||||||
self.client = self.app.client_manager.workflow_engine
|
self.client = self.app.client_manager.workflow_engine
|
||||||
|
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
# Copyright 2014 - Mirantis, Inc.
|
# Copyright 2014 - Mirantis, Inc.
|
||||||
# Copyright 2015 - StackStorm, Inc.
|
# Copyright 2015 - StackStorm, Inc.
|
||||||
# Copyright 2016 - Brocade Communications Systems, Inc.
|
# Copyright 2016 - Brocade Communications Systems, Inc.
|
||||||
#
|
# Copyright 2020 - Nokia Software.
|
||||||
# All Rights Reserved
|
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
# not use this file except in compliance with the License. You may obtain
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@@ -19,8 +18,6 @@
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
import pkg_resources as pkg
|
import pkg_resources as pkg
|
||||||
import six
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
@@ -35,10 +32,10 @@ EXEC_DICT = {
|
|||||||
'workflow_namespace': '',
|
'workflow_namespace': '',
|
||||||
'root_execution_id': '',
|
'root_execution_id': '',
|
||||||
'description': '',
|
'description': '',
|
||||||
'state': 'RUNNING',
|
'state': 'SUCCESS',
|
||||||
'state_info': None,
|
'state_info': None,
|
||||||
'created_at': '1',
|
'created_at': '2020-02-07 08:10:32',
|
||||||
'updated_at': '1',
|
'updated_at': '2020-02-07 08:10:41',
|
||||||
'task_execution_id': None
|
'task_execution_id': None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +52,8 @@ SUB_WF_EXEC = executions.Execution(
|
|||||||
'description': '',
|
'description': '',
|
||||||
'state': 'ERROR',
|
'state': 'ERROR',
|
||||||
'state_info': None,
|
'state_info': None,
|
||||||
'created_at': '1',
|
'created_at': '2020-02-07 08:10:32',
|
||||||
'updated_at': '1',
|
'updated_at': '2020-02-07 08:10:41',
|
||||||
'task_execution_id': 'abc'
|
'task_execution_id': 'abc'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -69,10 +66,11 @@ EX_RESULT = (
|
|||||||
'',
|
'',
|
||||||
'<none>',
|
'<none>',
|
||||||
'<none>',
|
'<none>',
|
||||||
'RUNNING',
|
'SUCCESS',
|
||||||
None,
|
None,
|
||||||
'1',
|
'2020-02-07 08:10:32',
|
||||||
'1'
|
'2020-02-07 08:10:41',
|
||||||
|
'0:00:09'
|
||||||
)
|
)
|
||||||
|
|
||||||
SUB_WF_EX_RESULT = (
|
SUB_WF_EX_RESULT = (
|
||||||
@@ -85,8 +83,9 @@ SUB_WF_EX_RESULT = (
|
|||||||
'ROOT_EXECUTION_ID',
|
'ROOT_EXECUTION_ID',
|
||||||
'ERROR',
|
'ERROR',
|
||||||
None,
|
None,
|
||||||
'1',
|
'2020-02-07 08:10:32',
|
||||||
'1'
|
'2020-02-07 08:10:41',
|
||||||
|
'0:00:09'
|
||||||
)
|
)
|
||||||
|
|
||||||
EXECS_LIST = [EXEC, SUB_WF_EXEC]
|
EXECS_LIST = [EXEC, SUB_WF_EXEC]
|
||||||
@@ -98,24 +97,12 @@ EXEC_WITH_PUBLISHED = executions.Execution(mock, EXEC_WITH_PUBLISHED_DICT)
|
|||||||
|
|
||||||
|
|
||||||
class TestCLIExecutionsV2(base.BaseCommandTest):
|
class TestCLIExecutionsV2(base.BaseCommandTest):
|
||||||
|
|
||||||
stdout = six.moves.StringIO()
|
|
||||||
stderr = six.moves.StringIO()
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestCLIExecutionsV2, self).setUp()
|
super(TestCLIExecutionsV2, self).setUp()
|
||||||
|
|
||||||
# Redirect stdout and stderr so it doesn't pollute the test result.
|
|
||||||
sys.stdout = self.stdout
|
|
||||||
sys.stderr = self.stderr
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestCLIExecutionsV2, self).tearDown()
|
super(TestCLIExecutionsV2, self).tearDown()
|
||||||
|
|
||||||
# Reset to original stdout and stderr.
|
|
||||||
sys.stdout = sys.__stdout__
|
|
||||||
sys.stderr = sys.__stderr__
|
|
||||||
|
|
||||||
def test_create_wf_input_string(self):
|
def test_create_wf_input_string(self):
|
||||||
self.client.executions.create.return_value = EXEC
|
self.client.executions.create.return_value = EXEC
|
||||||
|
|
||||||
@@ -124,10 +111,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
app_args=['id', '{ "context": true }']
|
app_args=['id', '{ "context": true }']
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(EX_RESULT, result[1])
|
||||||
EX_RESULT,
|
|
||||||
result[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_wf_input_file(self):
|
def test_create_wf_input_file(self):
|
||||||
self.client.executions.create.return_value = EXEC
|
self.client.executions.create.return_value = EXEC
|
||||||
@@ -142,10 +126,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
app_args=['id', path]
|
app_args=['id', path]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(EX_RESULT, result[1])
|
||||||
EX_RESULT,
|
|
||||||
result[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_with_description(self):
|
def test_create_with_description(self):
|
||||||
self.client.executions.create.return_value = EXEC
|
self.client.executions.create.return_value = EXEC
|
||||||
@@ -155,10 +136,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
app_args=['id', '{ "context": true }', '-d', '']
|
app_args=['id', '{ "context": true }', '-d', '']
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(EX_RESULT, result[1])
|
||||||
EX_RESULT,
|
|
||||||
result[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_update_state(self):
|
def test_update_state(self):
|
||||||
states = ['RUNNING', 'SUCCESS', 'PAUSED', 'ERROR', 'CANCELLED']
|
states = ['RUNNING', 'SUCCESS', 'PAUSED', 'ERROR', 'CANCELLED']
|
||||||
@@ -175,14 +153,18 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
'description': '',
|
'description': '',
|
||||||
'state': state,
|
'state': state,
|
||||||
'state_info': None,
|
'state_info': None,
|
||||||
'created_at': '1',
|
'created_at': '2020-02-07 08:10:32',
|
||||||
'updated_at': '1',
|
'updated_at': '2020-02-07 08:10:41',
|
||||||
'task_execution_id': None
|
'task_execution_id': None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ex_result = list(EX_RESULT)
|
ex_result = list(EX_RESULT)
|
||||||
ex_result[7] = state
|
ex_result[7] = state
|
||||||
|
|
||||||
|
# We'll ignore "duration" since for not terminal states
|
||||||
|
# it is unpredictable.
|
||||||
|
del ex_result[11]
|
||||||
ex_result = tuple(ex_result)
|
ex_result = tuple(ex_result)
|
||||||
|
|
||||||
result = self.call(
|
result = self.call(
|
||||||
@@ -190,10 +172,11 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
app_args=['id', '-s', state]
|
app_args=['id', '-s', state]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
result_ex = list(result[1])
|
||||||
ex_result,
|
del result_ex[11]
|
||||||
result[1]
|
result_ex = tuple(result_ex)
|
||||||
)
|
|
||||||
|
self.assertEqual(ex_result, result_ex)
|
||||||
|
|
||||||
def test_update_invalid_state(self):
|
def test_update_invalid_state(self):
|
||||||
states = ['IDLE', 'WAITING', 'DELAYED']
|
states = ['IDLE', 'WAITING', 'DELAYED']
|
||||||
@@ -214,10 +197,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
app_args=['id', '-s', 'RUNNING', '--env', '{"k1": "foobar"}']
|
app_args=['id', '-s', 'RUNNING', '--env', '{"k1": "foobar"}']
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(EX_RESULT, result[1])
|
||||||
EX_RESULT,
|
|
||||||
result[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_update_description(self):
|
def test_update_description(self):
|
||||||
self.client.executions.update.return_value = EXEC
|
self.client.executions.update.return_value = EXEC
|
||||||
@@ -227,10 +207,7 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
app_args=['id', '-d', 'foobar']
|
app_args=['id', '-d', 'foobar']
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(EX_RESULT, result[1])
|
||||||
EX_RESULT,
|
|
||||||
result[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
self.client.executions.list.return_value = [SUB_WF_EXEC, EXEC]
|
self.client.executions.list.return_value = [SUB_WF_EXEC, EXEC]
|
||||||
@@ -353,20 +330,14 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
|
|||||||
|
|
||||||
result = self.call(execution_cmd.Get, app_args=['id'])
|
result = self.call(execution_cmd.Get, app_args=['id'])
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(EX_RESULT, result[1])
|
||||||
EX_RESULT,
|
|
||||||
result[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_sub_wf_ex(self):
|
def test_get_sub_wf_ex(self):
|
||||||
self.client.executions.get.return_value = SUB_WF_EXEC
|
self.client.executions.get.return_value = SUB_WF_EXEC
|
||||||
|
|
||||||
result = self.call(execution_cmd.Get, app_args=['id'])
|
result = self.call(execution_cmd.Get, app_args=['id'])
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(SUB_WF_EX_RESULT, result[1])
|
||||||
SUB_WF_EX_RESULT,
|
|
||||||
result[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
self.call(execution_cmd.Delete, app_args=['id'])
|
self.call(execution_cmd.Delete, app_args=['id'])
|
||||||
|
@@ -47,8 +47,8 @@ TASK_SUB_WF_EXEC = Execution(
|
|||||||
'description': '',
|
'description': '',
|
||||||
'state': 'ERROR',
|
'state': 'ERROR',
|
||||||
'state_info': None,
|
'state_info': None,
|
||||||
'created_at': '1',
|
'created_at': '2020-02-07 08:10:32',
|
||||||
'updated_at': '1',
|
'updated_at': '2020-02-07 08:10:41',
|
||||||
'task_execution_id': '123'
|
'task_execution_id': '123'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -63,8 +63,9 @@ TASK_SUB_WF_EX_RESULT = (
|
|||||||
'ROOT_EXECUTION_ID',
|
'ROOT_EXECUTION_ID',
|
||||||
'ERROR',
|
'ERROR',
|
||||||
None,
|
None,
|
||||||
'1',
|
'2020-02-07 08:10:32',
|
||||||
'1'
|
'2020-02-07 08:10:41',
|
||||||
|
'0:00:09'
|
||||||
)
|
)
|
||||||
|
|
||||||
TASK_RESULT = {"test": "is", "passed": "successfully"}
|
TASK_RESULT = {"test": "is", "passed": "successfully"}
|
||||||
|
Reference in New Issue
Block a user