mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 10:27:37 -02:30
create jinja context based on job serialization
This commit is contained in:
@@ -227,6 +227,156 @@ class Notification(CreatedModifiedModel):
|
|||||||
|
|
||||||
|
|
||||||
class JobNotificationMixin(object):
|
class JobNotificationMixin(object):
|
||||||
|
# Tree of fields that can be safely referenced in a notification message
|
||||||
|
JOB_FIELDS_WHITELIST = ['id', 'type', 'url', 'created', 'modified', 'name', 'description', 'job_type', 'playbook',
|
||||||
|
'forks', 'limit', 'verbosity', 'job_tags', 'force_handlers', 'skip_tags', 'start_at_task',
|
||||||
|
'timeout', 'use_fact_cache', 'launch_type', 'status', 'failed', 'started', 'finished',
|
||||||
|
'elapsed', 'job_explanation', 'execution_node', 'controller_node', 'allow_simultaneous',
|
||||||
|
'scm_revision', 'diff_mode', 'job_slice_number', 'job_slice_count', 'custom_virtualenv',
|
||||||
|
{'host_status_counts': ['skipped', 'ok', 'changed', 'failures', 'dark']},
|
||||||
|
{'playbook_counts': ['play_count', 'task_count']},
|
||||||
|
{'summary_fields': [{'inventory': ['id', 'name', 'description', 'has_active_failures',
|
||||||
|
'total_hosts', 'hosts_with_active_failures', 'total_groups',
|
||||||
|
'groups_with_active_failures', 'has_inventory_sources',
|
||||||
|
'total_inventory_sources', 'inventory_sources_with_failures',
|
||||||
|
'organization_id', 'kind']},
|
||||||
|
{'project': ['id', 'name', 'description', 'status', 'scm_type']},
|
||||||
|
{'project_update': ['id', 'name', 'description', 'status', 'failed']},
|
||||||
|
{'job_template': ['id', 'name', 'description']},
|
||||||
|
{'unified_job_template': ['id', 'name', 'description', 'unified_job_type']},
|
||||||
|
{'instance_group': ['name', 'id']},
|
||||||
|
{'created_by': ['id', 'username', 'first_name', 'last_name']},
|
||||||
|
{'labels': ['count', 'results']},
|
||||||
|
{'source_workflow_job': ['description', 'elapsed', 'failed', 'id', 'name', 'status']}]}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def context_stub(cls):
|
||||||
|
"""Returns a stub context that can be used for validating notification messages.
|
||||||
|
Context has the same structure as the context that will actually be used to render
|
||||||
|
a notification message."""
|
||||||
|
context = {'job': {'allow_simultaneous': False,
|
||||||
|
'controller_node': 'foo_controller',
|
||||||
|
'created': datetime.datetime(2018, 11, 13, 6, 4, 0, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
'custom_virtualenv': 'my_venv',
|
||||||
|
'description': 'Sample job description',
|
||||||
|
'diff_mode': False,
|
||||||
|
'elapsed': 0.403018,
|
||||||
|
'execution_node': 'awx',
|
||||||
|
'failed': False,
|
||||||
|
'finished': False,
|
||||||
|
'force_handlers': False,
|
||||||
|
'forks': 0,
|
||||||
|
'host_status_counts': {'skipped': 1, 'ok': 5, 'changed': 3, 'failures': 0, 'dark': 0},
|
||||||
|
'id': 42,
|
||||||
|
'job_explanation': 'Sample job explanation',
|
||||||
|
'job_slice_count': 1,
|
||||||
|
'job_slice_number': 0,
|
||||||
|
'job_tags': '',
|
||||||
|
'job_type': 'run',
|
||||||
|
'launch_type': 'workflow',
|
||||||
|
'limit': 'bar_limit',
|
||||||
|
'modified': datetime.datetime(2018, 12, 13, 6, 4, 0, 0, tzinfo=datetime.timezone.utc),
|
||||||
|
'name': 'Stub JobTemplate',
|
||||||
|
'playbook_counts': {'play_count': 5, 'task_count': 10},
|
||||||
|
'playbook': 'ping.yml',
|
||||||
|
'scm_revision': '',
|
||||||
|
'skip_tags': '',
|
||||||
|
'start_at_task': '',
|
||||||
|
'started': '2019-07-29T17:38:14.137461Z',
|
||||||
|
'status': 'running',
|
||||||
|
'summary_fields': {'created_by': {'first_name': '',
|
||||||
|
'id': 1,
|
||||||
|
'last_name': '',
|
||||||
|
'username': 'admin'},
|
||||||
|
'instance_group': {'id': 1, 'name': 'tower'},
|
||||||
|
'inventory': {'description': 'Sample inventory description',
|
||||||
|
'groups_with_active_failures': 0,
|
||||||
|
'has_active_failures': False,
|
||||||
|
'has_inventory_sources': False,
|
||||||
|
'hosts_with_active_failures': 0,
|
||||||
|
'id': 17,
|
||||||
|
'inventory_sources_with_failures': 0,
|
||||||
|
'kind': '',
|
||||||
|
'name': 'Stub Inventory',
|
||||||
|
'organization_id': 121,
|
||||||
|
'total_groups': 0,
|
||||||
|
'total_hosts': 1,
|
||||||
|
'total_inventory_sources': 0},
|
||||||
|
'job_template': {'description': 'Sample job template description',
|
||||||
|
'id': 39,
|
||||||
|
'name': 'Stub JobTemplate'},
|
||||||
|
'labels': {'count': 0, 'results': []},
|
||||||
|
'project': {'description': 'Sample project description',
|
||||||
|
'id': 38,
|
||||||
|
'name': 'Stub project',
|
||||||
|
'scm_type': 'git',
|
||||||
|
'status': 'successful'},
|
||||||
|
'project_update': {'id': 5, 'name': 'Stub Project Update', 'description': 'Project Update',
|
||||||
|
'status': 'running', 'failed': False},
|
||||||
|
'unified_job_template': {'description': 'Sample unified job template description',
|
||||||
|
'id': 39,
|
||||||
|
'name': 'Stub Job Template',
|
||||||
|
'unified_job_type': 'job'},
|
||||||
|
'source_workflow_job': {'description': 'Sample workflow job description',
|
||||||
|
'elapsed': 0.000,
|
||||||
|
'failed': False,
|
||||||
|
'id': 88,
|
||||||
|
'name': 'Stub WorkflowJobTemplate',
|
||||||
|
'status': 'running'}},
|
||||||
|
'timeout': 0,
|
||||||
|
'type': 'job',
|
||||||
|
'url': '/api/v2/jobs/13/',
|
||||||
|
'use_fact_cache': False,
|
||||||
|
'verbosity': 0},
|
||||||
|
'job_friendly_name': 'Job',
|
||||||
|
'url': 'https://towerhost/#/jobs/playbook/1010',
|
||||||
|
'job_summary_dict': """{'url': 'https://towerhost/$/jobs/playbook/13',
|
||||||
|
'traceback': '',
|
||||||
|
'status': 'running',
|
||||||
|
'started': '2019-08-07T21:46:38.362630+00:00',
|
||||||
|
'project': 'Stub project',
|
||||||
|
'playbook': 'ping.yml',
|
||||||
|
'name': 'Stub Job Template',
|
||||||
|
'limit': '',
|
||||||
|
'inventory': 'Stub Inventory',
|
||||||
|
'id': 42,
|
||||||
|
'hosts': {},
|
||||||
|
'friendly_name': 'Job',
|
||||||
|
'finished': False,
|
||||||
|
'credential': 'Stub credential',
|
||||||
|
'created_by': 'admin'}"""}
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def context(self, serialized_job):
|
||||||
|
"""Returns a context that can be used for rendering notification messages.
|
||||||
|
Context contains whitelisted content retrieved from a serialized job object
|
||||||
|
(see JobNotificationMixin.JOB_FIELDS_WHITELIST), the job's friendly name,
|
||||||
|
and a url to the job run."""
|
||||||
|
context = {'job': {},
|
||||||
|
'job_friendly_name': self.get_notification_friendly_name(),
|
||||||
|
'url': self.get_ui_url(),
|
||||||
|
'job_summary_dict': json.dumps(self.notification_data(), indent=4)}
|
||||||
|
|
||||||
|
def build_context(node, fields, whitelisted_fields):
|
||||||
|
for safe_field in whitelisted_fields:
|
||||||
|
if type(safe_field) is dict:
|
||||||
|
field, whitelist_subnode = safe_field.copy().popitem()
|
||||||
|
# ensure content present in job serialization
|
||||||
|
if field not in fields:
|
||||||
|
continue
|
||||||
|
subnode = fields[field]
|
||||||
|
node[field] = {}
|
||||||
|
build_context(node[field], subnode, whitelist_subnode)
|
||||||
|
else:
|
||||||
|
# ensure content present in job serialization
|
||||||
|
if safe_field not in fields:
|
||||||
|
continue
|
||||||
|
node[safe_field] = fields[safe_field]
|
||||||
|
build_context(context['job'], serialized_job, self.JOB_FIELDS_WHITELIST)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
def get_notification_templates(self):
|
def get_notification_templates(self):
|
||||||
raise RuntimeError("Define me")
|
raise RuntimeError("Define me")
|
||||||
|
|
||||||
|
|||||||
148
awx/main/tests/functional/models/test_notifications.py
Normal file
148
awx/main/tests/functional/models/test_notifications.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from copy import deepcopy
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
#from awx.main.models import NotificationTemplates, Notifications, JobNotificationMixin
|
||||||
|
from awx.main.models import (AdHocCommand, InventoryUpdate, Job, JobNotificationMixin, ProjectUpdate,
|
||||||
|
SystemJob, WorkflowJob)
|
||||||
|
from awx.api.serializers import UnifiedJobSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class TestJobNotificationMixin(object):
|
||||||
|
CONTEXT_STRUCTURE = {'job': {'allow_simultaneous': bool,
|
||||||
|
'custom_virtualenv': str,
|
||||||
|
'controller_node': str,
|
||||||
|
'created': datetime.datetime,
|
||||||
|
'description': str,
|
||||||
|
'diff_mode': bool,
|
||||||
|
'elapsed': float,
|
||||||
|
'execution_node': str,
|
||||||
|
'failed': bool,
|
||||||
|
'finished': bool,
|
||||||
|
'force_handlers': bool,
|
||||||
|
'forks': int,
|
||||||
|
'host_status_counts': {'skipped': int, 'ok': int, 'changed': int,
|
||||||
|
'failures': int, 'dark': int},
|
||||||
|
'id': int,
|
||||||
|
'job_explanation': str,
|
||||||
|
'job_slice_count': int,
|
||||||
|
'job_slice_number': int,
|
||||||
|
'job_tags': str,
|
||||||
|
'job_type': str,
|
||||||
|
'launch_type': str,
|
||||||
|
'limit': str,
|
||||||
|
'modified': datetime.datetime,
|
||||||
|
'name': str,
|
||||||
|
'playbook': str,
|
||||||
|
'playbook_counts': {'play_count': int, 'task_count': int},
|
||||||
|
'scm_revision': str,
|
||||||
|
'skip_tags': str,
|
||||||
|
'start_at_task': str,
|
||||||
|
'started': str,
|
||||||
|
'status': str,
|
||||||
|
'summary_fields': {'created_by': {'first_name': str,
|
||||||
|
'id': int,
|
||||||
|
'last_name': str,
|
||||||
|
'username': str},
|
||||||
|
'instance_group': {'id': int, 'name': str},
|
||||||
|
'inventory': {'description': str,
|
||||||
|
'groups_with_active_failures': int,
|
||||||
|
'has_active_failures': bool,
|
||||||
|
'has_inventory_sources': bool,
|
||||||
|
'hosts_with_active_failures': int,
|
||||||
|
'id': int,
|
||||||
|
'inventory_sources_with_failures': int,
|
||||||
|
'kind': str,
|
||||||
|
'name': str,
|
||||||
|
'organization_id': int,
|
||||||
|
'total_groups': int,
|
||||||
|
'total_hosts': int,
|
||||||
|
'total_inventory_sources': int},
|
||||||
|
'job_template': {'description': str,
|
||||||
|
'id': int,
|
||||||
|
'name': str},
|
||||||
|
'labels': {'count': int, 'results': list},
|
||||||
|
'project': {'description': str,
|
||||||
|
'id': int,
|
||||||
|
'name': str,
|
||||||
|
'scm_type': str,
|
||||||
|
'status': str},
|
||||||
|
'project_update': {'id': int, 'name': str, 'description': str, 'status': str, 'failed': bool},
|
||||||
|
'unified_job_template': {'description': str,
|
||||||
|
'id': int,
|
||||||
|
'name': str,
|
||||||
|
'unified_job_type': str},
|
||||||
|
'source_workflow_job': {'description': str,
|
||||||
|
'elapsed': float,
|
||||||
|
'failed': bool,
|
||||||
|
'id': int,
|
||||||
|
'name': str,
|
||||||
|
'status': str}},
|
||||||
|
|
||||||
|
'timeout': int,
|
||||||
|
'type': str,
|
||||||
|
'url': str,
|
||||||
|
'use_fact_cache': bool,
|
||||||
|
'verbosity': int},
|
||||||
|
'job_friendly_name': str,
|
||||||
|
'job_summary_dict': str,
|
||||||
|
'url': str}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize('JobClass', [AdHocCommand, InventoryUpdate, Job, ProjectUpdate, SystemJob, WorkflowJob])
|
||||||
|
def test_context(self, JobClass, sqlite_copy_expert, project, inventory_source):
|
||||||
|
"""The Jinja context defines all of the fields that can be used by a template. Ensure that the context generated
|
||||||
|
for each job type has the expected structure."""
|
||||||
|
def check_structure(expected_structure, obj):
|
||||||
|
if isinstance(expected_structure, dict):
|
||||||
|
assert isinstance(obj, dict)
|
||||||
|
for key in obj:
|
||||||
|
assert key in expected_structure
|
||||||
|
if obj[key] is None:
|
||||||
|
continue
|
||||||
|
if isinstance(expected_structure[key], dict):
|
||||||
|
assert isinstance(obj[key], dict)
|
||||||
|
check_structure(expected_structure[key], obj[key])
|
||||||
|
else:
|
||||||
|
assert isinstance(obj[key], expected_structure[key])
|
||||||
|
kwargs = {}
|
||||||
|
if JobClass is InventoryUpdate:
|
||||||
|
kwargs['inventory_source'] = inventory_source
|
||||||
|
elif JobClass is ProjectUpdate:
|
||||||
|
kwargs['project'] = project
|
||||||
|
|
||||||
|
job = JobClass.objects.create(name='foo', **kwargs)
|
||||||
|
job_serialization = UnifiedJobSerializer(job).to_representation(job)
|
||||||
|
|
||||||
|
context = job.context(job_serialization)
|
||||||
|
check_structure(TestJobNotificationMixin.CONTEXT_STRUCTURE, context)
|
||||||
|
|
||||||
|
def test_context_stub(self):
|
||||||
|
"""The context stub is a fake context used to validate custom notification messages. Ensure that
|
||||||
|
this also has the expected structure. Furthermore, ensure that the stub context contains
|
||||||
|
*all* fields that could possibly be included in a context."""
|
||||||
|
def check_structure_and_completeness(expected_structure, obj):
|
||||||
|
expected_structure = deepcopy(expected_structure)
|
||||||
|
if isinstance(expected_structure, dict):
|
||||||
|
assert isinstance(obj, dict)
|
||||||
|
for key in obj:
|
||||||
|
assert key in expected_structure
|
||||||
|
# Context stub should not have any undefined fields
|
||||||
|
assert obj[key] is not None
|
||||||
|
if isinstance(expected_structure[key], dict):
|
||||||
|
assert isinstance(obj[key], dict)
|
||||||
|
check_structure_and_completeness(expected_structure[key], obj[key])
|
||||||
|
expected_structure.pop(key)
|
||||||
|
else:
|
||||||
|
assert isinstance(obj[key], expected_structure[key])
|
||||||
|
expected_structure.pop(key)
|
||||||
|
# Ensure all items in expected structure were present
|
||||||
|
assert not len(expected_structure)
|
||||||
|
|
||||||
|
context_stub = JobNotificationMixin.context_stub()
|
||||||
|
check_structure_and_completeness(TestJobNotificationMixin.CONTEXT_STRUCTURE, context_stub)
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user