mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 14:57:39 -02:30
render notification templates
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user