mirror of
https://github.com/ansible/awx.git
synced 2024-10-30 22:21:13 +03:00
render notification templates
This commit is contained in:
parent
1a1eab4dab
commit
8158632344
@ -13,6 +13,10 @@ from datetime import timedelta
|
||||
from oauthlib import oauth2
|
||||
from oauthlib.common import generate_token
|
||||
|
||||
# Jinja
|
||||
from jinja2 import sandbox, StrictUndefined
|
||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
@ -46,16 +50,16 @@ from awx.main.constants import (
|
||||
CENSOR_VALUE,
|
||||
)
|
||||
from awx.main.models import (
|
||||
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential, CredentialInputSource,
|
||||
CredentialType, CustomInventoryScript, Group, Host, Instance,
|
||||
InstanceGroup, Inventory, InventorySource, InventoryUpdate,
|
||||
InventoryUpdateEvent, Job, JobEvent, JobHostSummary, JobLaunchConfig,
|
||||
JobTemplate, Label, Notification, NotificationTemplate,
|
||||
OAuth2AccessToken, OAuth2Application, Organization, Project,
|
||||
ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
|
||||
SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob,
|
||||
UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
|
||||
WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded
|
||||
ActivityStream, AdHocCommand, AdHocCommandEvent, Credential,
|
||||
CredentialInputSource, CredentialType, CustomInventoryScript,
|
||||
Group, Host, Instance, InstanceGroup, Inventory, InventorySource,
|
||||
InventoryUpdate, InventoryUpdateEvent, Job, JobEvent, JobHostSummary,
|
||||
JobLaunchConfig, JobNotificationMixin, JobTemplate, Label, Notification,
|
||||
NotificationTemplate, OAuth2AccessToken, OAuth2Application, Organization,
|
||||
Project, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule,
|
||||
StdoutMaxBytesExceeded, SystemJob, SystemJobEvent, SystemJobTemplate,
|
||||
Team, UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode,
|
||||
WorkflowJobTemplate, WorkflowJobTemplateNode
|
||||
)
|
||||
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
|
||||
from awx.main.models.rbac import (
|
||||
@ -4197,6 +4201,37 @@ class NotificationTemplateSerializer(BaseSerializer):
|
||||
continue
|
||||
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):
|
||||
from awx.api.views import NotificationTemplateDetail
|
||||
|
||||
|
@ -48,7 +48,10 @@ from awx.main.models.mixins import ( # noqa
|
||||
TaskManagerJobMixin, TaskManagerProjectUpdateMixin,
|
||||
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.workflow import ( # noqa
|
||||
WorkflowJob, WorkflowJobNode, WorkflowJobOptions, WorkflowJobTemplate,
|
||||
|
@ -2,7 +2,9 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
from copy import deepcopy
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
@ -10,6 +12,8 @@ from django.core.mail.message import EmailMessage
|
||||
from django.db import connection
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import smart_str, force_text
|
||||
from jinja2 import sandbox
|
||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
|
||||
|
||||
# AWX
|
||||
from awx.api.versioning import reverse
|
||||
@ -74,6 +78,36 @@ class NotificationTemplate(CommonModelNameNotUnique):
|
||||
default=dict,
|
||||
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):
|
||||
return reverse('api:notification_template_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
@ -227,6 +261,9 @@ class Notification(CreatedModifiedModel):
|
||||
|
||||
|
||||
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
|
||||
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',
|
||||
@ -383,50 +420,65 @@ class JobNotificationMixin(object):
|
||||
def get_notification_friendly_name(self):
|
||||
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_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()
|
||||
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)
|
||||
|
||||
def build_notification_succeeded_message(self):
|
||||
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):
|
||||
def send_notification_templates(self, status):
|
||||
from awx.main.tasks import send_notifications # avoid circular import
|
||||
if status_str not in ['succeeded', 'failed', 'running']:
|
||||
raise ValueError(_("status_str must be either running, succeeded or failed"))
|
||||
if status not in ['running', 'succeeded', 'failed']:
|
||||
raise ValueError(_("status must be either running, succeeded or failed"))
|
||||
|
||||
try:
|
||||
notification_templates = self.get_notification_templates()
|
||||
except Exception:
|
||||
logger.warn("No notification template defined for emitting notification")
|
||||
notification_templates = None
|
||||
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)
|
||||
return
|
||||
|
||||
def send_it():
|
||||
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
|
||||
for n in all_notification_templates],
|
||||
job_id=self.id)
|
||||
connection.on_commit(send_it)
|
||||
if not notification_templates:
|
||||
return
|
||||
|
||||
for nt in set(notification_templates.get(self.STATUS_TO_TEMPLATE_TYPE[status], [])):
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user