Implement client side of event list --nested-depth

This change does the following:
- cleans up the usage of get_events so that marker and limit are only
  specified in their dedicated arguments, not also in event_args
  (also, specifying only in event_args still works)
- first attempts server-side nested_depth support
- falls back to the old recursive approach if the response data lacks a
  link with the ref root_stack

Since there is a fallback for old heat APIs, the client change can
land before or after the heat change
I27e1ffb770e00a7f929c081b2a505e2007f5d584

Change-Id: I43d12ec61ec359222184f07c170de3c97481f1ba
Closes-Bug: #1588561
This commit is contained in:
Steve Baker 2016-06-09 08:47:17 +12:00
parent a654226dd1
commit 31278ff5f7
6 changed files with 186 additions and 47 deletions

View File

@ -67,29 +67,57 @@ def get_hook_events(hc, stack_id, event_args, nested_depth=0,
def get_events(hc, stack_id, event_args, nested_depth=0,
marker=None, limit=None):
event_args = dict(event_args)
if marker:
event_args['marker'] = marker
if limit:
event_args['limit'] = limit
if not nested_depth:
# simple call with no nested_depth
return _get_stack_events(hc, stack_id, event_args)
# assume an API which supports nested_depth
event_args['nested_depth'] = nested_depth
events = _get_stack_events(hc, stack_id, event_args)
if nested_depth > 0:
events.extend(_get_nested_events(hc, nested_depth,
stack_id, event_args))
# Because there have been multiple stacks events mangled into
# one list, we need to sort before passing to print_list
# Note we can't use the prettytable sortby_index here, because
# the "start" option doesn't allow post-sort slicing, which
# will be needed to make "--marker" work for nested_depth lists
events.sort(key=lambda x: x.event_time)
# Slice the list if marker is specified
if marker:
try:
marker_index = [e.id for e in events].index(marker)
events = events[marker_index:]
except ValueError:
pass
if not events:
return events
# Slice the list if limit is specified
if limit:
limit_index = min(int(limit), len(events))
events = events[:limit_index]
first_links = getattr(events[0], 'links', [])
root_stack_link = [l for l in first_links
if l.get('rel') == 'root_stack']
if root_stack_link:
# response has a root_stack link, indicating this is an API which
# supports nested_depth
return events
# API doesn't support nested_depth, do client-side paging and recursive
# event fetch
marker = event_args.pop('marker', None)
limit = event_args.pop('limit', None)
event_args.pop('nested_depth', None)
events = _get_stack_events(hc, stack_id, event_args)
events.extend(_get_nested_events(hc, nested_depth,
stack_id, event_args))
# Because there have been multiple stacks events mangled into
# one list, we need to sort before passing to print_list
# Note we can't use the prettytable sortby_index here, because
# the "start" option doesn't allow post-sort slicing, which
# will be needed to make "--marker" work for nested_depth lists
events.sort(key=lambda x: x.event_time)
# Slice the list if marker is specified
if marker:
try:
marker_index = [e.id for e in events].index(marker)
events = events[marker_index:]
except ValueError:
pass
# Slice the list if limit is specified
if limit:
limit_index = min(int(limit), len(events))
events = events[:limit_index]
return events

View File

@ -157,8 +157,6 @@ class ListEvent(lister.Lister):
kwargs = {
'resource_name': parsed_args.resource,
'limit': parsed_args.limit,
'marker': parsed_args.marker,
'filters': heat_utils.format_parameters(parsed_args.filter),
'sort_dir': 'asc'
}
@ -168,10 +166,6 @@ class ListEvent(lister.Lister):
raise exc.CommandError(msg)
if parsed_args.nested_depth:
# Until the API supports recursive event listing we'll have to do
# the marker/limit filtering client-side
del kwargs['marker']
del kwargs['limit']
columns.append('stack_name')
nested_depth = parsed_args.nested_depth
else:
@ -185,7 +179,6 @@ class ListEvent(lister.Lister):
marker = parsed_args.marker
try:
while True:
kwargs['marker'] = marker
events = event_utils.get_events(
client,
stack_id=parsed_args.stack,

View File

@ -342,8 +342,8 @@ class UpdateStack(show.ShowOne):
# find the last event to use as the marker
events = event_utils.get_events(client,
stack_id=parsed_args.stack,
event_args={'sort_dir': 'desc',
'limit': 1})
event_args={'sort_dir': 'desc'},
limit=1)
marker = events[0].id if events else None
client.stacks.update(**fields)
@ -660,8 +660,8 @@ class DeleteStack(command.Command):
events = event_utils.get_events(heat_client,
stack_id=sid,
event_args={
'sort_dir': 'desc',
'limit': 1})
'sort_dir': 'desc'},
limit=1)
if events:
marker = events[0].id
except heat_exc.CommandError as ex:
@ -1025,8 +1025,8 @@ def _stack_action(stack, parsed_args, heat_client, action, action_name=None):
# find the last event to use as the marker
events = event_utils.get_events(heat_client,
stack_id=stack,
event_args={'sort_dir': 'desc',
'limit': 1})
event_args={'sort_dir': 'desc'},
limit=1)
marker = events[0].id if events else None
try:

View File

@ -99,8 +99,6 @@ class TestEventList(TestEvent):
defaults = {
'stack_id': 'my_stack',
'resource_name': None,
'limit': None,
'marker': None,
'filters': {},
'sort_dir': 'asc'
}
@ -169,8 +167,7 @@ class TestEventList(TestEvent):
def test_event_list_nested_depth(self):
arglist = ['my_stack', '--nested-depth', '3', '--format', 'table']
kwargs = copy.deepcopy(self.defaults)
del kwargs['marker']
del kwargs['limit']
kwargs['nested_depth'] = 3
cols = copy.deepcopy(self.fields)
cols[-1] = 'stack_name'
cols.append('logical_resource_id')
@ -178,7 +175,10 @@ class TestEventList(TestEvent):
columns, data = self.cmd.take_action(parsed_args)
self.event_client.list.assert_called_with(**kwargs)
self.event_client.list.assert_has_calls([
mock.call(**kwargs),
mock.call(**self.defaults)
])
self.assertEqual(cols, columns)
def test_event_list_sort(self):

View File

@ -2767,14 +2767,18 @@ class ShellTestEventsNested(ShellBase):
for r in required:
self.assertRegex(list_text, r)
def _stub_event_list_response(self, stack_id, nested_id, timestamps):
def _stub_event_list_response_old_api(self, stack_id, nested_id,
timestamps, first_request):
# Stub events for parent stack
ev_resp_dict = {"events": [{"id": "p_eventid1",
"event_time": timestamps[0]},
{"id": "p_eventid2",
"event_time": timestamps[3]}]}
self.mock_request_get('/stacks/%s/events?sort_dir=asc' % stack_id,
ev_resp_dict)
self.mock_request_get(first_request, ev_resp_dict)
# response lacks root_stack link, fetch nested events recursively
self.mock_request_get('/stacks/%s/events?sort_dir=asc'
% stack_id, ev_resp_dict)
# Stub resources for parent, including one nested
res_resp_dict = {"resources": [
@ -2794,7 +2798,7 @@ class ShellTestEventsNested(ShellBase):
self.mock_request_get('/stacks/%s/events?sort_dir=asc' % nested_id,
nev_resp_dict)
def test_shell_nested_depth(self):
def test_shell_nested_depth_old_api(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_id = 'nested/2'
@ -2802,7 +2806,10 @@ class ShellTestEventsNested(ShellBase):
"2014-01-06T16:15:00Z", # nested n_eventid1
"2014-01-06T16:16:00Z", # nested n_eventid2
"2014-01-06T16:17:00Z") # parent p_eventid2
self._stub_event_list_response(stack_id, nested_id, timestamps)
first_request = ('/stacks/%s/events?nested_depth=1&sort_dir=asc'
% stack_id)
self._stub_event_list_response_old_api(
stack_id, nested_id, timestamps, first_request)
self.m.ReplayAll()
list_text = self.shell('event-list %s --nested-depth 1' % stack_id)
required = ['id', 'p_eventid1', 'p_eventid2', 'n_eventid1',
@ -2814,7 +2821,7 @@ class ShellTestEventsNested(ShellBase):
self.assertRegex(list_text,
"%s.*\n.*%s.*\n.*%s.*\n.*%s" % timestamps)
def test_shell_nested_depth_marker(self):
def test_shell_nested_depth_marker_old_api(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_id = 'nested/2'
@ -2822,7 +2829,10 @@ class ShellTestEventsNested(ShellBase):
"2014-01-06T16:15:00Z", # nested n_eventid1
"2014-01-06T16:16:00Z", # nested n_eventid2
"2014-01-06T16:17:00Z") # parent p_eventid2
self._stub_event_list_response(stack_id, nested_id, timestamps)
first_request = ('/stacks/%s/events?marker=n_eventid1&nested_depth=1'
'&sort_dir=asc' % stack_id)
self._stub_event_list_response_old_api(
stack_id, nested_id, timestamps, first_request)
self.m.ReplayAll()
list_text = self.shell(
'event-list %s --nested-depth 1 --marker n_eventid1' % stack_id)
@ -2836,7 +2846,7 @@ class ShellTestEventsNested(ShellBase):
self.assertRegex(list_text,
"%s.*\n.*%s.*\n.*%s.*" % timestamps[1:])
def test_shell_nested_depth_limit(self):
def test_shell_nested_depth_limit_old_api(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_id = 'nested/2'
@ -2844,7 +2854,10 @@ class ShellTestEventsNested(ShellBase):
"2014-01-06T16:15:00Z", # nested n_eventid1
"2014-01-06T16:16:00Z", # nested n_eventid2
"2014-01-06T16:17:00Z") # parent p_eventid2
self._stub_event_list_response(stack_id, nested_id, timestamps)
first_request = ('/stacks/%s/events?limit=2&nested_depth=1'
'&sort_dir=asc' % stack_id)
self._stub_event_list_response_old_api(
stack_id, nested_id, timestamps, first_request)
self.m.ReplayAll()
list_text = self.shell(
'event-list %s --nested-depth 1 --limit 2' % stack_id)
@ -2858,6 +2871,104 @@ class ShellTestEventsNested(ShellBase):
self.assertRegex(list_text,
"%s.*\n.*%s.*\n" % timestamps[:2])
def _nested_events(self):
links = [
{"rel": "self"},
{"rel": "resource"},
{"rel": "stack"},
{"rel": "root_stack"}
]
return [
{
"id": "p_eventid1",
"event_time": '2014-01-06T16:14:00Z',
"stack_id": '1',
"resource_name": 'the_stack',
"resource_status": 'CREATE_IN_PROGRESS',
"resource_status_reason": 'Stack CREATE started',
"links": links,
}, {
"id": 'n_eventid1',
"event_time": '2014-01-06T16:15:00Z',
"stack_id": '2',
"resource_name": 'nested_stack',
"resource_status": 'CREATE_IN_PROGRESS',
"resource_status_reason": 'Stack CREATE started',
"links": links,
}, {
"id": 'n_eventid2',
"event_time": '2014-01-06T16:16:00Z',
"stack_id": '2',
"resource_name": 'nested_stack',
"resource_status": 'CREATE_COMPLETE',
"resource_status_reason": 'Stack CREATE completed',
"links": links,
}, {
"id": "p_eventid2",
"event_time": '2014-01-06T16:17:00Z',
"stack_id": '1',
"resource_name": 'the_stack',
"resource_status": 'CREATE_COMPLETE',
"resource_status_reason": 'Stack CREATE completed',
"links": links,
},
]
def test_shell_nested_depth(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_events = self._nested_events()
ev_resp_dict = {'events': nested_events}
url = '/stacks/%s/events?nested_depth=1&sort_dir=asc' % stack_id
self.mock_request_get(url, ev_resp_dict)
self.m.ReplayAll()
list_text = self.shell('event-list %s --nested-depth 1 --format log'
% stack_id)
self.assertEqual('''\
2014-01-06 16:14:00Z [the_stack]: CREATE_IN_PROGRESS Stack CREATE started
2014-01-06 16:15:00Z [nested_stack]: CREATE_IN_PROGRESS Stack CREATE started
2014-01-06 16:16:00Z [nested_stack]: CREATE_COMPLETE Stack CREATE completed
2014-01-06 16:17:00Z [the_stack]: CREATE_COMPLETE Stack CREATE completed
''', list_text)
def test_shell_nested_depth_marker(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_events = self._nested_events()
ev_resp_dict = {'events': nested_events[1:]}
url = ('/stacks/%s/events?marker=n_eventid1&nested_depth=1'
'&sort_dir=asc' % stack_id)
self.mock_request_get(url, ev_resp_dict)
self.m.ReplayAll()
list_text = self.shell('event-list %s --nested-depth 1 --format log '
'--marker n_eventid1'
% stack_id)
self.assertEqual('''\
2014-01-06 16:15:00Z [nested_stack]: CREATE_IN_PROGRESS Stack CREATE started
2014-01-06 16:16:00Z [nested_stack]: CREATE_COMPLETE Stack CREATE completed
2014-01-06 16:17:00Z [the_stack]: CREATE_COMPLETE Stack CREATE completed
''', list_text)
def test_shell_nested_depth_limit(self):
self.register_keystone_auth_fixture()
stack_id = 'teststack/1'
nested_events = self._nested_events()
ev_resp_dict = {'events': nested_events[:2]}
url = ('/stacks/%s/events?limit=2&nested_depth=1&sort_dir=asc'
% stack_id)
self.mock_request_get(url, ev_resp_dict)
self.m.ReplayAll()
list_text = self.shell('event-list %s --nested-depth 1 --format log '
'--limit 2'
% stack_id)
self.assertEqual('''\
2014-01-06 16:14:00Z [the_stack]: CREATE_IN_PROGRESS Stack CREATE started
2014-01-06 16:15:00Z [nested_stack]: CREATE_IN_PROGRESS Stack CREATE started
''', list_text)
class ShellTestHookFunctions(ShellBase):
def setUp(self):
@ -2892,6 +3003,10 @@ class ShellTestHookFunctions(ShellBase):
"event_time": "2014-01-06T16:17:00Z",
"resource_name": "p_res",
"resource_status_reason": hook_reason}]}
url = '/stacks/%s/events?nested_depth=1&sort_dir=asc' % stack_id
self.mock_request_get(url, ev_resp_dict)
# this api doesn't support nested_depth, fetch events recursively
self.mock_request_get('/stacks/%s/events?sort_dir=asc' % stack_id,
ev_resp_dict)

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
from oslo_utils import encodeutils
import six
from six.moves.urllib import parse
@ -65,6 +66,8 @@ class EventManager(stacks.StackChildManager):
parse.quote(stack_id, ''),
parse.quote(encodeutils.safe_encode(resource_name), ''))
if params:
# convert to a sorted dict for python3 predictible order
params = collections.OrderedDict(sorted(params.items()))
url += '?%s' % parse.urlencode(params, True)
return self._list(url, 'events')