render notification templates

This commit is contained in:
Jim Ladd
2019-08-14 11:13:27 -07:00
parent 1a1eab4dab
commit 8158632344
3 changed files with 138 additions and 48 deletions

View File

@@ -13,6 +13,10 @@ from datetime import timedelta
from oauthlib import oauth2 from oauthlib import oauth2
from oauthlib.common import generate_token from oauthlib.common import generate_token
# Jinja
from jinja2 import sandbox, StrictUndefined
from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
# Django # Django
from django.conf import settings from django.conf import settings
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
@@ -46,16 +50,16 @@ from awx.main.constants import (
CENSOR_VALUE, CENSOR_VALUE,
) )
from awx.main.models import ( from awx.main.models import (
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialInputSource, ActivityStream, AdHocCommand, AdHocCommandEvent, Credential,
CredentialType, CustomInventoryScript, Group, Host, Instance, CredentialInputSource, CredentialType, CustomInventoryScript,
InstanceGroup, Inventory, InventorySource, InventoryUpdate, Group, Host, Instance, InstanceGroup, Inventory, InventorySource,
InventoryUpdateEvent, Job, JobEvent, JobHostSummary, JobLaunchConfig, InventoryUpdate, InventoryUpdateEvent, Job, JobEvent, JobHostSummary,
JobTemplate, Label, Notification, NotificationTemplate, JobLaunchConfig, JobNotificationMixin, JobTemplate, Label, Notification,
OAuth2AccessToken, OAuth2Application, Organization, Project, NotificationTemplate, OAuth2AccessToken, OAuth2Application, Organization,
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule, Project, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob, StdoutMaxBytesExceeded, SystemJob, SystemJobEvent, SystemJobTemplate,
UnifiedJobTemplate, WorkflowJob, WorkflowJobNode, Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded WorkflowJobTemplate, WorkflowJobTemplateNode
) )
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
from awx.main.models.rbac import ( from awx.main.models.rbac import (
@@ -4197,6 +4201,37 @@ class NotificationTemplateSerializer(BaseSerializer):
continue continue
collected_messages.append(message) collected_messages.append(message)
# Subclass to return name of undefined field
class DescriptiveUndefined(StrictUndefined):
# The parent class prevents _accessing attributes_ of an object
# but will render undefined objects with 'Undefined'. This
# prevents their use entirely.
__repr__ = __str__ = StrictUndefined._fail_with_undefined_error
def __init__(self, *args, **kwargs):
super(DescriptiveUndefined, self).__init__(*args, **kwargs)
# When an undefined field is encountered, return the name
# of the undefined field in the exception message
# (StrictUndefined refers to the explicitly set exception
# message as the 'hint')
self._undefined_hint = self._undefined_name
# Ensure messages can be rendered
for msg in collected_messages:
env = sandbox.ImmutableSandboxedEnvironment(undefined=DescriptiveUndefined)
try:
env.from_string(msg).render(JobNotificationMixin.context_stub())
except TemplateSyntaxError as exc:
error_list.append(_("Unable to render message '{}': {}".format(msg, exc.message)))
except UndefinedError as exc:
error_list.append(_("Field '{}' unavailable".format(exc.message)))
except SecurityError as exc:
error_list.append(_("Security error due to field '{}'".format(exc.message)))
if error_list:
raise serializers.ValidationError(error_list)
return messages
def validate(self, attrs): def validate(self, attrs):
from awx.api.views import NotificationTemplateDetail from awx.api.views import NotificationTemplateDetail

View File

@@ -48,7 +48,10 @@ from awx.main.models.mixins import ( # noqa
TaskManagerJobMixin, TaskManagerProjectUpdateMixin, TaskManagerJobMixin, TaskManagerProjectUpdateMixin,
TaskManagerUnifiedJobMixin, TaskManagerUnifiedJobMixin,
) )
from awx.main.models.notifications import Notification, NotificationTemplate # noqa from awx.main.models.notifications import ( # noqa
Notification, NotificationTemplate,
JobNotificationMixin
)
from awx.main.models.label import Label # noqa from awx.main.models.label import Label # noqa
from awx.main.models.workflow import ( # noqa from awx.main.models.workflow import ( # noqa
WorkflowJob, WorkflowJobNode, WorkflowJobOptions, WorkflowJobTemplate, WorkflowJob, WorkflowJobNode, WorkflowJobOptions, WorkflowJobTemplate,

View File

@@ -2,7 +2,9 @@
# All Rights Reserved. # All Rights Reserved.
from copy import deepcopy from copy import deepcopy
import datetime
import logging import logging
import json
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
@@ -10,6 +12,8 @@ from django.core.mail.message import EmailMessage
from django.db import connection from django.db import connection
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_str, force_text from django.utils.encoding import smart_str, force_text
from jinja2 import sandbox
from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
# AWX # AWX
from awx.api.versioning import reverse from awx.api.versioning import reverse
@@ -74,6 +78,36 @@ class NotificationTemplate(CommonModelNameNotUnique):
default=dict, default=dict,
help_text=_('Optional custom messages for notification template.')) help_text=_('Optional custom messages for notification template.'))
def has_message(self, condition):
potential_template = self.messages.get(condition, {})
if potential_template == {}:
return False
if potential_template.get('message', {}) == {}:
return False
return True
def get_message(self, condition):
return self.messages.get(condition, {})
def build_notification_message(self, event_type, context):
env = sandbox.ImmutableSandboxedEnvironment()
templates = self.get_message(event_type)
msg_template = templates.get('message', {})
try:
notification_subject = env.from_string(msg_template).render(**context)
except (TemplateSyntaxError, UndefinedError, SecurityError):
notification_subject = ''
msg_body = templates.get('body', {})
try:
notification_body = env.from_string(msg_body).render(**context)
except (TemplateSyntaxError, UndefinedError, SecurityError):
notification_body = ''
return (notification_subject, notification_body)
def get_absolute_url(self, request=None): def get_absolute_url(self, request=None):
return reverse('api:notification_template_detail', kwargs={'pk': self.pk}, request=request) return reverse('api:notification_template_detail', kwargs={'pk': self.pk}, request=request)
@@ -227,6 +261,9 @@ class Notification(CreatedModifiedModel):
class JobNotificationMixin(object): class JobNotificationMixin(object):
STATUS_TO_TEMPLATE_TYPE = {'succeeded': 'success',
'running': 'started',
'failed': 'error'}
# Tree of fields that can be safely referenced in a notification message # 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', 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', 'forks', 'limit', 'verbosity', 'job_tags', 'force_handlers', 'skip_tags', 'start_at_task',
@@ -383,50 +420,65 @@ class JobNotificationMixin(object):
def get_notification_friendly_name(self): def get_notification_friendly_name(self):
raise RuntimeError("Define me") raise RuntimeError("Define me")
def _build_notification_message(self, status_str): def notification_data(self):
raise RuntimeError("Define me")
def build_notification_message(self, nt, status):
env = sandbox.ImmutableSandboxedEnvironment()
from awx.api.serializers import UnifiedJobSerializer
job_serialization = UnifiedJobSerializer(self).to_representation(self)
context = self.context(job_serialization)
msg_template = body_template = None
if nt.messages:
templates = nt.messages.get(self.STATUS_TO_TEMPLATE_TYPE[status], {}) or {}
msg_template = templates.get('message', {})
body_template = templates.get('body', {})
if msg_template:
try:
notification_subject = env.from_string(msg_template).render(**context)
except (TemplateSyntaxError, UndefinedError, SecurityError):
notification_subject = ''
else:
notification_subject = u"{} #{} '{}' {}: {}".format(self.get_notification_friendly_name(),
self.id,
self.name,
status,
self.get_ui_url())
notification_body = self.notification_data() notification_body = self.notification_data()
notification_subject = u"{} #{} '{}' {}: {}".format(self.get_notification_friendly_name(),
self.id,
self.name,
status_str,
notification_body['url'])
notification_body['friendly_name'] = self.get_notification_friendly_name() notification_body['friendly_name'] = self.get_notification_friendly_name()
if body_template:
try:
notification_body['body'] = env.from_string(body_template).render(**context)
except (TemplateSyntaxError, UndefinedError, SecurityError):
notification_body['body'] = ''
return (notification_subject, notification_body) return (notification_subject, notification_body)
def build_notification_succeeded_message(self): def send_notification_templates(self, status):
return self._build_notification_message('succeeded')
def build_notification_failed_message(self):
return self._build_notification_message('failed')
def build_notification_running_message(self):
return self._build_notification_message('running')
def send_notification_templates(self, status_str):
from awx.main.tasks import send_notifications # avoid circular import from awx.main.tasks import send_notifications # avoid circular import
if status_str not in ['succeeded', 'failed', 'running']: if status not in ['running', 'succeeded', 'failed']:
raise ValueError(_("status_str must be either running, succeeded or failed")) raise ValueError(_("status must be either running, succeeded or failed"))
try: try:
notification_templates = self.get_notification_templates() notification_templates = self.get_notification_templates()
except Exception: except Exception:
logger.warn("No notification template defined for emitting notification") logger.warn("No notification template defined for emitting notification")
notification_templates = None return
if notification_templates:
if status_str == 'succeeded':
notification_template_type = 'success'
elif status_str == 'running':
notification_template_type = 'started'
else:
notification_template_type = 'error'
all_notification_templates = set(notification_templates.get(notification_template_type, []))
if len(all_notification_templates):
try:
(notification_subject, notification_body) = getattr(self, 'build_notification_%s_message' % status_str)()
except AttributeError:
raise NotImplementedError("build_notification_%s_message() does not exist" % status_str)
def send_it(): if not notification_templates:
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id return
for n in all_notification_templates],
job_id=self.id) for nt in set(notification_templates.get(self.STATUS_TO_TEMPLATE_TYPE[status], [])):
connection.on_commit(send_it) try:
(notification_subject, notification_body) = self.build_notification_message(nt, status)
except AttributeError:
raise NotImplementedError("build_notification_message() does not exist" % status)
def send_it():
send_notifications.delay([nt.generate_notification(notification_subject, notification_body).id],
job_id=self.id)
connection.on_commit(send_it)