From 550d9d5e42a605a23cb540584bf439c07c4185d4 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 26 May 2022 11:37:23 -0400 Subject: [PATCH 1/2] detect if job events are tree-like and collapsable in the UI --- awx/api/views/__init__.py | 30 +++++++++++- awx/main/tests/functional/api/test_events.py | 51 +++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index f864ab2d5ed2..f39c627c484c 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3850,7 +3850,7 @@ class JobJobEventsChildrenSummary(APIView): meta_events = ('debug', 'verbose', 'warning', 'error', 'system_warning', 'deprecated') def get(self, request, **kwargs): - resp = dict(children_summary={}, meta_event_nested_uuid={}, event_processing_finished=False) + resp = dict(children_summary={}, meta_event_nested_uuid={}, event_processing_finished=False, is_tree=True) job = get_object_or_404(models.Job, pk=kwargs['pk']) if not job.event_processing_finished: return Response(resp) @@ -3870,13 +3870,41 @@ def get(self, request, **kwargs): # key is counter of meta events (i.e. verbose), value is uuid of the assigned parent map_meta_counter_nested_uuid = {} + # collapsable tree view in the UI only makes sense for tree-like + # hierarchy. If ansible is ran with a strategy like free or host_pinned, then + # events can be out of sequential order, and no longer follow a tree structure + # E1 + # E2 + # E3 + # E4 <- parent is E3 + # E5 <- parent is E1 + # in the above, there is no clear way to collapse E1, because E5 comes after + # E3, which occurs after E1. Thus the tree view should be disabled. + + # mark the last seen uuid at a given level (0-3) + # if a parent uuid is not in this list, then we know the events are not tree-like + # and return a response with is_tree: False + level_current_uuid = [None, None, None, None] + prev_non_meta_event = events[0] for i, e in enumerate(events): if not e['event'] in JobJobEventsChildrenSummary.meta_events: prev_non_meta_event = e if not e['uuid']: continue + + if not e['event'] in JobJobEventsChildrenSummary.meta_events: + level = models.JobEvent.LEVEL_FOR_EVENT[e['event']] + level_current_uuid[level] = e['uuid'] + # if setting level 1, for example, set levels 2 and 3 back to None + for u in range(level + 1, len(level_current_uuid)): + level_current_uuid[u] = None + puuid = e['parent_uuid'] + if puuid and puuid not in level_current_uuid: + # improper tree detected, so bail out early + resp['is_tree'] = False + return Response(resp) # if event is verbose (or debug, etc), we need to "assign" it a # parent. This code looks at the event level of the previous diff --git a/awx/main/tests/functional/api/test_events.py b/awx/main/tests/functional/api/test_events.py index ce65a20d8030..34ecf4d691cc 100644 --- a/awx/main/tests/functional/api/test_events.py +++ b/awx/main/tests/functional/api/test_events.py @@ -70,11 +70,11 @@ def test_job_job_events_children_summary(get, organization_factory, job_template job_id=job.pk, uuid='uuid2', parent_uuid='uuid1', event="playbook_on_play_start", counter=2, stdout='a' * 1024, job_created=job.created ).save() JobEvent.create_from_data( - job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="runner_on_start", counter=3, stdout='a' * 1024, job_created=job.created + job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="playbook_on_task_start", counter=3, stdout='a' * 1024, job_created=job.created ).save() JobEvent.create_from_data(job_id=job.pk, uuid='uuid4', parent_uuid='', event='verbose', counter=4, stdout='a' * 1024, job_created=job.created).save() JobEvent.create_from_data( - job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_task_start", counter=5, stdout='a' * 1024, job_created=job.created + job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_play_start", counter=5, stdout='a' * 1024, job_created=job.created ).save() job.emitted_events = job.get_event_queryset().count() job.status = "successful" @@ -84,3 +84,50 @@ def test_job_job_events_children_summary(get, organization_factory, job_template assert response.data["children_summary"] == {1: {"rowNumber": 0, "numChildren": 4}, 2: {"rowNumber": 1, "numChildren": 2}} assert response.data["meta_event_nested_uuid"] == {4: "uuid2"} assert response.data["event_processing_finished"] == True + assert response.data["is_tree"] == True + + +@pytest.mark.django_db +def test_job_job_events_children_summary_is_tree(get, organization_factory, job_template_factory): + ''' + children_summary should return {is_tree: False} if the event structure is not tree-like + ''' + objs = organization_factory("org", superusers=['admin']) + jt = job_template_factory("jt", organization=objs.organization, inventory='test_inv', project='test_proj').job_template + job = jt.create_unified_job() + url = reverse('api:job_job_events_children_summary', kwargs={'pk': job.pk}) + response = get(url, user=objs.superusers.admin, expect=200) + assert response.data["event_processing_finished"] == False + ''' + E1 + E2 + E3 + E4 (verbose) + E5 + E6 <-- parent is E2, but comes after another "branch" E5 + ''' + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid1', parent_uuid='', event="playbook_on_start", counter=1, stdout='a' * 1024, job_created=job.created + ).save() + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid2', parent_uuid='uuid1', event="playbook_on_play_start", counter=2, stdout='a' * 1024, job_created=job.created + ).save() + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="playbook_on_task_start", counter=3, stdout='a' * 1024, job_created=job.created + ).save() + JobEvent.create_from_data(job_id=job.pk, uuid='uuid4', parent_uuid='', event='verbose', counter=4, stdout='a' * 1024, job_created=job.created).save() + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_play_start", counter=5, stdout='a' * 1024, job_created=job.created + ).save() + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid6', parent_uuid='uuid2', event="playbook_on_task_start", counter=6, stdout='a' * 1024, job_created=job.created + ).save() + job.emitted_events = job.get_event_queryset().count() + job.status = "successful" + job.save() + url = reverse('api:job_job_events_children_summary', kwargs={'pk': job.pk}) + response = get(url, user=objs.superusers.admin, expect=200) + assert response.data["children_summary"] == {} + assert response.data["meta_event_nested_uuid"] == {} + assert response.data["event_processing_finished"] == True + assert response.data["is_tree"] == False From 2704b202bf87132bfd4219d80fe01842e102aa82 Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Wed, 1 Jun 2022 10:36:06 -0700 Subject: [PATCH 2/2] check for is_tree flag from children summary response --- awx/ui/src/screens/Job/JobOutput/useJobEvents.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/src/screens/Job/JobOutput/useJobEvents.js b/awx/ui/src/screens/Job/JobOutput/useJobEvents.js index c3f894279e22..7566c1a915d2 100644 --- a/awx/ui/src/screens/Job/JobOutput/useJobEvents.js +++ b/awx/ui/src/screens/Job/JobOutput/useJobEvents.js @@ -56,7 +56,8 @@ export default function useJobEvents(callbacks, jobId, isFlatMode) { callbacks .fetchChildrenSummary() .then((result) => { - if (result.data.event_processing_finished === false) { + const { event_processing_finished, is_tree } = result.data; + if (event_processing_finished === false || is_tree === false) { callbacks.setForceFlatMode(true); callbacks.setJobTreeReady(); return;