1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 08:21:15 +03:00

Render WF approval notifications w/ custom templates

This commit is contained in:
Jim Ladd 2019-10-15 23:19:12 -07:00 committed by Ryan Petrello
parent 4e9ec271c5
commit 4809c40f3c
No known key found for this signature in database
GPG Key ID: F2AA5F2122351777
7 changed files with 141 additions and 54 deletions

View File

@ -4338,25 +4338,12 @@ class NotificationTemplateSerializer(BaseSerializer):
error_list = []
collected_messages = []
# Validate structure / content types
if not isinstance(messages, dict):
error_list.append(_("Expected dict for 'messages' field, found {}".format(type(messages))))
else:
for event in messages:
if event not in ['started', 'success', 'error']:
error_list.append(_("Event '{}' invalid, must be one of 'started', 'success', or 'error'").format(event))
continue
event_messages = messages[event]
if event_messages is None:
continue
if not isinstance(event_messages, dict):
error_list.append(_("Expected dict for event '{}', found {}").format(event, type(event_messages)))
continue
for message_type in event_messages:
if message_type not in ['message', 'body']:
def check_messages(messages):
for message_type in messages:
if message_type not in ('message', 'body'):
error_list.append(_("Message type '{}' invalid, must be either 'message' or 'body'").format(message_type))
continue
message = event_messages[message_type]
message = messages[message_type]
if message is None:
continue
if not isinstance(message, str):
@ -4368,6 +4355,36 @@ class NotificationTemplateSerializer(BaseSerializer):
continue
collected_messages.append(message)
# Validate structure / content types
if not isinstance(messages, dict):
error_list.append(_("Expected dict for 'messages' field, found {}".format(type(messages))))
else:
for event in messages:
if event not in ('started', 'success', 'error', 'workflow_approval'):
error_list.append(_("Event '{}' invalid, must be one of 'started', 'success', 'error', or 'workflow_approval'").format(event))
continue
event_messages = messages[event]
if event_messages is None:
continue
if not isinstance(event_messages, dict):
error_list.append(_("Expected dict for event '{}', found {}").format(event, type(event_messages)))
continue
if event == 'workflow_approval':
for subevent in event_messages:
if subevent not in ('running', 'approved', 'timed_out', 'denied'):
error_list.append(_("Workflow Approval event '{}' invalid, must be one of "
"'running', 'approved', 'timed_out', or 'denied'").format(subevent))
continue
subevent_messages = event_messages[subevent]
if subevent_messages is None:
continue
if not isinstance(subevent_messages, dict):
error_list.append(_("Expected dict for workflow approval event '{}', found {}").format(subevent, type(subevent_messages)))
continue
check_messages(subevent_messages)
else:
check_messages(event_messages)
# Subclass to return name of undefined field
class DescriptiveUndefined(StrictUndefined):
# The parent class prevents _accessing attributes_ of an object

View File

@ -73,7 +73,7 @@ class NotificationTemplate(CommonModelNameNotUnique):
notification_configuration = prevent_search(JSONField(blank=False))
def default_messages():
return {'started': None, 'success': None, 'error': None}
return {'started': None, 'success': None, 'error': None, 'workflow_approval': None}
messages = JSONField(
null=True,
@ -109,19 +109,34 @@ class NotificationTemplate(CommonModelNameNotUnique):
old_messages = old_nt.messages
new_messages = self.messages
def merge_messages(local_old_messages, local_new_messages, local_event):
if local_new_messages.get(local_event, {}) and local_old_messages.get(local_event, {}):
local_old_event_msgs = local_old_messages[local_event]
local_new_event_msgs = local_new_messages[local_event]
for msg_type in ['message', 'body']:
if msg_type not in local_new_event_msgs and local_old_event_msgs.get(msg_type, None):
local_new_event_msgs[msg_type] = local_old_event_msgs[msg_type]
if old_messages is not None and new_messages is not None:
for event in ['started', 'success', 'error']:
for event in ('started', 'success', 'error', 'workflow_approval'):
if not new_messages.get(event, {}) and old_messages.get(event, {}):
new_messages[event] = old_messages[event]
continue
if new_messages.get(event, {}) and old_messages.get(event, {}):
old_event_msgs = old_messages[event]
new_event_msgs = new_messages[event]
for msg_type in ['message', 'body']:
if msg_type not in new_event_msgs and old_event_msgs.get(msg_type, None):
new_event_msgs[msg_type] = old_event_msgs[msg_type]
if event == 'workflow_approval' and old_messages.get('workflow_approval', None):
new_messages.setdefault('workflow_approval', {})
for subevent in ('running', 'approved', 'timed_out', 'denied'):
old_wfa_messages = old_messages['workflow_approval']
new_wfa_messages = new_messages['workflow_approval']
if not new_wfa_messages.get(subevent, {}) and old_wfa_messages.get(subevent, {}):
new_wfa_messages[subevent] = old_wfa_messages[subevent]
continue
if old_wfa_messages:
merge_messages(old_wfa_messages, new_wfa_messages, subevent)
else:
merge_messages(old_messages, new_messages, event)
new_messages.setdefault(event, None)
for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password",
self.notification_class.init_parameters):
if self.notification_configuration[field].startswith("$encrypted$"):
@ -370,8 +385,8 @@ class JobNotificationMixin(object):
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
"""Returns a dictionary that can be used for rendering notification messages.
The context will contain 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': {},
@ -419,14 +434,15 @@ class JobNotificationMixin(object):
# Use custom template if available
if nt.messages:
templates = nt.messages.get(self.STATUS_TO_TEMPLATE_TYPE[status], {}) or {}
msg_template = templates.get('message', None)
body_template = templates.get('body', None)
template = nt.messages.get(self.STATUS_TO_TEMPLATE_TYPE[status], {}) or {}
msg_template = template.get('message', None)
body_template = template.get('body', None)
# If custom template not provided, look up default template
default_template = nt.notification_class.default_messages[self.STATUS_TO_TEMPLATE_TYPE[status]]
if not msg_template:
msg_template = getattr(nt.notification_class, 'DEFAULT_MSG', None)
msg_template = default_template.get('message', None)
if not body_template:
body_template = getattr(nt.notification_class, 'DEFAULT_BODY', None)
body_template = default_template.get('body', None)
if msg_template:
try:

View File

@ -2,6 +2,7 @@
# All Rights Reserved.
# Python
import json
import logging
from copy import copy
from urllib.parse import urljoin
@ -16,6 +17,9 @@ from django.core.exceptions import ObjectDoesNotExist
# Django-CRUM
from crum import get_current_user
from jinja2 import sandbox
from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
# AWX
from awx.api.versioning import reverse
from awx.main.models import (prevent_search, accepts_json, UnifiedJobTemplate,
@ -763,22 +767,45 @@ class WorkflowApproval(UnifiedJob, JobNotificationMixin):
connection.on_commit(send_it())
def build_approval_notification_message(self, nt, approval_status):
subject = []
workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id))
subject.append(('The approval node "{}"').format(self.workflow_approval_template.name))
if approval_status == 'running':
subject.append(('needs review. This node can be viewed at: {}').format(workflow_url))
if approval_status == 'approved':
subject.append(('was approved. {}').format(workflow_url))
if approval_status == 'timed_out':
subject.append(('has timed out. {}').format(workflow_url))
elif approval_status == 'denied':
subject.append(('was denied. {}').format(workflow_url))
subject = " ".join(subject)
body = self.notification_data()
body['body'] = subject
env = sandbox.ImmutableSandboxedEnvironment()
return subject, body
context = self.context(approval_status)
msg_template = body_template = None
msg = body = ''
# Use custom template if available
if nt.messages and nt.messages.get('workflow_approval', None):
template = nt.messages['workflow_approval'].get(approval_status, {})
msg_template = template.get('message', None)
body_template = template.get('body', None)
# If custom template not provided, look up default template
default_template = nt.notification_class.default_messages['workflow_approval'][approval_status]
if not msg_template:
msg_template = default_template.get('message', None)
if not body_template:
body_template = default_template.get('body', None)
if msg_template:
try:
msg = env.from_string(msg_template).render(**context)
except (TemplateSyntaxError, UndefinedError, SecurityError):
msg = ''
if body_template:
try:
body = env.from_string(body_template).render(**context)
except (TemplateSyntaxError, UndefinedError, SecurityError):
body = ''
return (msg, body)
def context(self, approval_status):
workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id))
return {'approval_status': approval_status,
'approval_node_name': self.workflow_approval_template.name,
'workflow_url': workflow_url,
'job_summary_dict': json.dumps(self.notification_data(), indent=4)}
@property
def workflow_job_template(self):

View File

@ -11,4 +11,13 @@ class CustomNotificationBase(object):
default_messages = {"started": {"message": DEFAULT_MSG, "body": None},
"success": {"message": DEFAULT_MSG, "body": None},
"error": {"message": DEFAULT_MSG, "body": None}}
"error": {"message": DEFAULT_MSG, "body": None},
"workflow_approval": {"running": {"message": 'The approval node "{{ approval_node_name }}" needs review. '
'This node can be viewed at: {{ workflow_url }}',
"body": None},
"approved": {"message": 'The approval node "{{ approval_node_name }}" was approved. {{ workflow_url }}',
"body": None},
"timed_out": {"message": 'The approval node "{{ approval_node_name }}" has timed out. {{ workflow_url }}',
"body": None},
"denied": {"message": 'The approval node "{{ approval_node_name }}" was denied. {{ workflow_url }}',
"body": None}}}

View File

@ -4,7 +4,9 @@
from django.core.mail.backends.smtp import EmailBackend
from awx.main.notifications.custom_notification_base import CustomNotificationBase
from CustomNotificationBase import DEFAULT_MSG, DEFAULT_BODY
DEFAULT_MSG = CustomNotificationBase.DEFAULT_MSG
DEFAULT_BODY = CustomNotificationBase.DEFAULT_BODY
class CustomEmailBackend(EmailBackend, CustomNotificationBase):
@ -23,7 +25,11 @@ class CustomEmailBackend(EmailBackend, CustomNotificationBase):
default_messages = {"started": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"success": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"error": {"message": DEFAULT_MSG, "body": DEFAULT_BODY}}
"error": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"workflow_approval": {"running": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"approved": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"timed_out": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"denied": {"message": DEFAULT_MSG, "body": DEFAULT_BODY}}}
def format_body(self, body):
# leave body unchanged (expect a string)

View File

@ -10,7 +10,9 @@ from django.utils.translation import ugettext_lazy as _
from awx.main.notifications.base import AWXBaseEmailBackend
from awx.main.notifications.custom_notification_base import CustomNotificationBase
from CustomNotificationBase import DEFAULT_MSG
DEFAULT_BODY = CustomNotificationBase.DEFAULT_BODY
DEFAULT_MSG = CustomNotificationBase.DEFAULT_MSG
logger = logging.getLogger('awx.main.notifications.pagerduty_backend')
@ -25,9 +27,13 @@ class PagerDutyBackend(AWXBaseEmailBackend, CustomNotificationBase):
sender_parameter = "client_name"
DEFAULT_BODY = "{{ job_summary_dict }}"
default_messages = {"started": { "message": DEFAULT_MSG, "body": DEFAULT_BODY},
"success": { "message": DEFAULT_MSG, "body": DEFAULT_BODY},
"error": { "message": DEFAULT_MSG, "body": DEFAULT_BODY}}
default_messages = {"started": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"success": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"error": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"workflow_approval": {"running": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"approved": {"message": DEFAULT_MSG,"body": DEFAULT_BODY},
"timed_out": {"message": DEFAULT_MSG, "body": DEFAULT_BODY},
"denied": {"message": DEFAULT_MSG, "body": DEFAULT_BODY}}}
def __init__(self, subdomain, token, fail_silently=False, **kwargs):
super(PagerDutyBackend, self).__init__(fail_silently=fail_silently)

View File

@ -29,7 +29,13 @@ class WebhookBackend(AWXBaseEmailBackend, CustomNotificationBase):
DEFAULT_BODY = "{{ job_summary_dict }}"
default_messages = {"started": {"body": DEFAULT_BODY},
"success": {"body": DEFAULT_BODY},
"error": {"body": DEFAULT_BODY}}
"error": {"body": DEFAULT_BODY},
"workflow_approval": {
"running": {"body": '{"body": "The approval node \\"{{ approval_node_name }}\\" needs review. '
'This node can be viewed at: {{ workflow_url }}"}'},
"approved": {"body": '{"body": "The approval node \\"{{ approval_node_name }}\\" was approved. {{ workflow_url }}"}'},
"timed_out": {"body": '{"body": "The approval node \\"{{ approval_node_name }}\\" has timed out. {{ workflow_url }}"}'},
"denied": {"body": '{"body": "The approval node \\"{{ approval_node_name }}\\" was denied. {{ workflow_url }}"}'}}}
def __init__(self, http_method, headers, disable_ssl_verification=False, fail_silently=False, username=None, password=None, **kwargs):
self.http_method = http_method