From ab3669efa9735eea8d532e1c0789453e4493765e Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Mon, 22 Feb 2016 17:09:36 -0500 Subject: [PATCH] Refactor message generator * Job object can now control the output and generate K:V output for notification types that can support it * Notifications store the body as json/dict now to encode more information * Notification Type can further compose the message based on what is sensible for the notification type * This will also allow customizing the message template in the future * All notification types use sane defaults for the level of detail now --- awx/api/serializers.py | 2 +- awx/api/views.py | 3 ++- awx/main/models/jobs.py | 20 ++++++++++++++++++++ awx/main/models/notifications.py | 17 +++++++---------- awx/main/models/unified_jobs.py | 10 ++++++++++ awx/main/notifications/email_backend.py | 7 +++++++ awx/main/notifications/hipchat_backend.py | 6 +++--- awx/main/notifications/irc_backend.py | 4 ++-- awx/main/notifications/pagerduty_backend.py | 7 +++++-- awx/main/notifications/slack_backend.py | 6 +++--- awx/main/notifications/twilio_backend.py | 6 +++--- awx/main/notifications/webhook_backend.py | 13 +++++++++---- awx/main/tasks.py | 12 ++++-------- 13 files changed, 76 insertions(+), 37 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 04f5e241c3..a680e5b00c 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2118,7 +2118,7 @@ class NotificationSerializer(BaseSerializer): class Meta: model = Notification fields = ('*', '-name', '-description', 'notifier', 'error', 'status', 'notifications_sent', - 'notification_type', 'recipients', 'subject', 'body') + 'notification_type', 'recipients', 'subject') def get_related(self, obj): res = super(NotificationSerializer, self).get_related(obj) diff --git a/awx/api/views.py b/awx/api/views.py index 439de3f845..70532f026c 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3053,7 +3053,8 @@ class NotifierTest(GenericAPIView): def post(self, request, *args, **kwargs): obj = self.get_object() - notification = obj.generate_notification("Tower Notification Test", "Ansible Tower Test Notification") + notification = obj.generate_notification("Tower Notification Test {}".format(obj.id), + {"body": "Ansible Tower Test Notification {}".format(obj.id)}) if not notification: return Response({}, status=status.HTTP_400_BAD_REQUEST) else: diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index dd772d695d..2d2dc991a9 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -496,6 +496,26 @@ class Job(UnifiedJob, JobOptions): dependencies.append(source.create_inventory_update(launch_type='dependency')) return dependencies + def notification_data(self): + data = super(Job, self).notification_data() + all_hosts = {} + for h in self.job_host_summaries.all(): + all_hosts[h.host.name] = dict(failed=h.failed, + changed=h.changed, + dark=h.dark, + failures=h.failures, + ok=h.ok, + processed=h.processed, + skipped=h.skipped) + data.update(dict(inventory=self.inventory.name, + project=self.project.name, + playbook=self.playbook, + credential=self.credential.name, + limit=self.limit, + extra_vars=self.extra_vars, + hosts=all_hosts)) + return data + def handle_extra_data(self, extra_data): extra_vars = {} if type(extra_data) == dict: diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index d6fc9d31b8..04bd5b0e53 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -69,8 +69,8 @@ class Notifier(CommonModel): for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password", self.notification_class.init_parameters): if new_instance: - value = getattr(self.notification_configuration, field, '') - setattr(self, '_saved_{}'.format(field), value) + value = self.notification_configuration[field] + setattr(self, '_saved_{}_{}'.format("config", field), value) self.notification_configuration[field] = '' else: encrypted = encrypt_field(self, 'notification_configuration', subfield=field) @@ -82,8 +82,9 @@ class Notifier(CommonModel): update_fields = [] for field in filter(lambda x: self.notification_class.init_parameters[x]['type'] == "password", self.notification_class.init_parameters): - saved_value = getattr(self, '_saved_{}'.format(field), '') - setattr(self.notification_configuration, field, saved_value) + saved_value = getattr(self, '_saved_{}_{}'.format("config", field), '') + self.notification_configuration[field] = saved_value + #setattr(self.notification_configuration, field, saved_value) if 'notification_configuration' not in update_fields: update_fields.append('notification_configuration') self.save(update_fields=update_fields) @@ -112,7 +113,7 @@ class Notifier(CommonModel): recipients = [recipients] sender = self.notification_configuration.pop(self.notification_class.sender_parameter, None) backend_obj = self.notification_class(**self.notification_configuration) - notification_obj = EmailMessage(subject, body, sender, recipients) + notification_obj = EmailMessage(subject, backend_obj.format_body(body), sender, recipients) return backend_obj.send_messages([notification_obj]) class Notification(CreatedModifiedModel): @@ -165,11 +166,7 @@ class Notification(CreatedModifiedModel): default='', editable=False, ) - body = models.TextField( - blank=True, - default='', - editable=False, - ) + body = JSONField(blank=True) def get_absolute_url(self): return reverse('api:notification_detail', args=(self.pk,)) diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 986be923fb..ed34653048 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -731,6 +731,16 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique tasks that might preclude creating one''' return [] + def notification_data(self): + return dict(id=self.id, + name=self.name, + url=self.get_absolute_url(), #TODO: Need to replace with UI job view + created_by=str(self.created_by), + started=self.started.isoformat(), + finished=self.finished.isoformat(), + status=self.status, + traceback=self.result_traceback) + def start(self, error_callback, success_callback, **kwargs): ''' Start the task running via Celery. diff --git a/awx/main/notifications/email_backend.py b/awx/main/notifications/email_backend.py index 271f585d5c..484a61f12d 100644 --- a/awx/main/notifications/email_backend.py +++ b/awx/main/notifications/email_backend.py @@ -18,3 +18,10 @@ class CustomEmailBackend(EmailBackend): recipient_parameter = "recipients" sender_parameter = "sender" + def format_body(self, body): + body_actual = "{} #{} had status {} on Ansible Tower, view details at {}\n\n".format(body['friendly_name'], + body['id'], + body['status'], + body['url']) + body_actual += pprint.pformat(body, indent=4) + return body_actual diff --git a/awx/main/notifications/hipchat_backend.py b/awx/main/notifications/hipchat_backend.py index a5b7f561b6..5d58792591 100644 --- a/awx/main/notifications/hipchat_backend.py +++ b/awx/main/notifications/hipchat_backend.py @@ -5,11 +5,11 @@ import logging import requests -from django.core.mail.backends.base import BaseEmailBackend +from awx.main.notifications.base import TowerBaseEmailBackend logger = logging.getLogger('awx.main.notifications.hipchat_backend') -class HipChatBackend(BaseEmailBackend): +class HipChatBackend(TowerBaseEmailBackend): init_parameters = {"token": {"label": "Token", "type": "password"}, "channels": {"label": "Destination Channels", "type": "list"}, @@ -35,7 +35,7 @@ class HipChatBackend(BaseEmailBackend): r = requests.post("{}/v2/room/{}/notification".format(self.api_url, rcp), params={"auth_token": self.token}, json={"color": self.color, - "message": m.body, + "message": m.subject, "notify": self.notify, "from": m.from_email, "message_format": "text"}) diff --git a/awx/main/notifications/irc_backend.py b/awx/main/notifications/irc_backend.py index 2b0944b74a..b3e92a12b3 100644 --- a/awx/main/notifications/irc_backend.py +++ b/awx/main/notifications/irc_backend.py @@ -7,11 +7,11 @@ import logging import irc.client -from django.core.mail.backends.base import BaseEmailBackend +from awx.main.notifications.base import TowerBaseEmailBackend logger = logging.getLogger('awx.main.notifications.irc_backend') -class IrcBackend(BaseEmailBackend): +class IrcBackend(TowerBaseEmailBackend): init_parameters = {"server": {"label": "IRC Server Address", "type": "string"}, "port": {"label": "IRC Server Port", "type": "int"}, diff --git a/awx/main/notifications/pagerduty_backend.py b/awx/main/notifications/pagerduty_backend.py index 161bb822bc..fd7661ba86 100644 --- a/awx/main/notifications/pagerduty_backend.py +++ b/awx/main/notifications/pagerduty_backend.py @@ -4,11 +4,11 @@ import logging import pygerduty -from django.core.mail.backends.base import BaseEmailBackend +from awx.main.notifications.base import TowerBaseEmailBackend logger = logging.getLogger('awx.main.notifications.pagerduty_backend') -class PagerDutyBackend(BaseEmailBackend): +class PagerDutyBackend(TowerBaseEmailBackend): init_parameters = {"subdomain": {"label": "Pagerduty subdomain", "type": "string"}, "token": {"label": "API Token", "type": "password"}, @@ -22,6 +22,9 @@ class PagerDutyBackend(BaseEmailBackend): self.subdomain = subdomain self.token = token + def format_body(self, body): + return body + def send_messages(self, messages): sent_messages = 0 diff --git a/awx/main/notifications/slack_backend.py b/awx/main/notifications/slack_backend.py index 3bf4f32114..91e4cd4fd3 100644 --- a/awx/main/notifications/slack_backend.py +++ b/awx/main/notifications/slack_backend.py @@ -4,11 +4,11 @@ import logging from slackclient import SlackClient -from django.core.mail.backends.base import BaseEmailBackend +from awx.main.notifications.base import TowerBaseEmailBackend logger = logging.getLogger('awx.main.notifications.slack_backend') -class SlackBackend(BaseEmailBackend): +class SlackBackend(TowerBaseEmailBackend): init_parameters = {"token": {"label": "Token", "type": "password"}, "channels": {"label": "Destination Channels", "type": "list"}} @@ -41,7 +41,7 @@ class SlackBackend(BaseEmailBackend): for m in messages: try: for r in m.recipients(): - self.connection.rtm_send_message(r, m.body) + self.connection.rtm_send_message(r, m.subject) sent_messages += 1 except Exception as e: logger.error("Exception sending messages: {}".format(e)) diff --git a/awx/main/notifications/twilio_backend.py b/awx/main/notifications/twilio_backend.py index d9c4cc43b6..847ebb9f2f 100644 --- a/awx/main/notifications/twilio_backend.py +++ b/awx/main/notifications/twilio_backend.py @@ -5,11 +5,11 @@ import logging from twilio.rest import TwilioRestClient -from django.core.mail.backends.base import BaseEmailBackend +from awx.main.notifications.base import TowerBaseEmailBackend logger = logging.getLogger('awx.main.notifications.twilio_backend') -class TwilioBackend(BaseEmailBackend): +class TwilioBackend(TowerBaseEmailBackend): init_parameters = {"account_sid": {"label": "Account SID", "type": "string"}, "account_token": {"label": "Account Token", "type": "password"}, @@ -38,7 +38,7 @@ class TwilioBackend(BaseEmailBackend): connection.messages.create( to=m.to, from_=m.from_email, - body=m.body) + body=m.subject) sent_messages += 1 except Exception as e: logger.error("Exception sending messages: {}".format(e)) diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index 5bdbff0e02..15cd950923 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -4,12 +4,12 @@ import logging import requests - -from django.core.mail.backends.base import BaseEmailBackend +import json +from awx.main.notifications.base import TowerBaseEmailBackend logger = logging.getLogger('awx.main.notifications.webhook_backend') -class WebhookBackend(BaseEmailBackend): +class WebhookBackend(TowerBaseEmailBackend): init_parameters = {"url": {"label": "Target URL", "type": "string"}, "headers": {"label": "HTTP Headers", "type": "object"}} @@ -20,11 +20,16 @@ class WebhookBackend(BaseEmailBackend): self.headers = headers super(WebhookBackend, self).__init__(fail_silently=fail_silently) + def format_body(self, body): + logger.error("Generating body from {}".format(str(body))) + return body + def send_messages(self, messages): sent_messages = 0 for m in messages: + logger.error("BODY: " + str(m.body)) r = requests.post("{}".format(m.recipients()[0]), - data=m.body, + data=json.dumps(m.body), headers=self.headers) if r.status_code >= 400: logger.error("Error sending notification webhook: {}".format(r.text)) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index aff9a6a585..7db56d78f5 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -207,10 +207,8 @@ def handle_work_success(self, result, task_actual): notification_subject = "{} #{} '{}' succeeded on Ansible Tower".format(friendly_name, task_actual['id'], instance_name) - notification_body = "{} #{} '{}' succeeded on Ansible Tower\nTo view the output: {}".format(friendly_name, - task_actual['id'], - instance_name, - instance.get_absolute_url()) + notification_body = instance.notification_data() + notification_body['friendly_name'] = friendly_name send_notifications.delay([n.generate_notification(notification_subject, notification_body) for n in notifiers.get('success', []) + notifiers.get('any', [])], job_id=task_actual['id']) @@ -265,10 +263,8 @@ def handle_work_error(self, task_id, subtasks=None): notification_subject = "{} #{} '{}' failed on Ansible Tower".format(first_task_friendly_name, first_task_id, first_task_name) - notification_body = "{} #{} '{}' failed on Ansible Tower\nTo view the output: {}".format(first_task_friendly_name, - first_task_id, - first_task_name, - first_task.get_absolute_url()) + notification_body = first_task.notification_data() + notification_body['friendly_name'] = first_task_friendly_name send_notifications.delay([n.generate_notification(notification_subject, notification_body).id for n in notifiers.get('error', []) + notifiers.get('any', [])], job_id=first_task_id)