diff --git a/awx/main/migrations/0002_squashed_v300_release.py b/awx/main/migrations/0002_squashed_v300_release.py index c398d18468..1b0348600c 100644 --- a/awx/main/migrations/0002_squashed_v300_release.py +++ b/awx/main/migrations/0002_squashed_v300_release.py @@ -157,7 +157,7 @@ class Migration(migrations.Migration): ('status', models.CharField(default=b'pending', max_length=20, editable=False, choices=[(b'pending', 'Pending'), (b'successful', 'Successful'), (b'failed', 'Failed')])), ('error', models.TextField(default=b'', editable=False, blank=True)), ('notifications_sent', models.IntegerField(default=0, editable=False)), - ('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'irc', 'IRC')])), + ('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'irc', 'IRC')])), ('recipients', models.TextField(default=b'', editable=False, blank=True)), ('subject', models.TextField(default=b'', editable=False, blank=True)), ('body', jsonfield.fields.JSONField(default=dict, blank=True)), @@ -174,7 +174,7 @@ class Migration(migrations.Migration): ('modified', models.DateTimeField(default=None, editable=False)), ('description', models.TextField(default=b'', blank=True)), ('name', models.CharField(unique=True, max_length=512)), - ('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'irc', 'IRC')])), + ('notification_type', models.CharField(max_length=32, choices=[(b'email', 'Email'), (b'slack', 'Slack'), (b'twilio', 'Twilio'), (b'pagerduty', 'Pagerduty'), (b'hipchat', 'HipChat'), (b'webhook', 'Webhook'), (b'mattermost', 'Mattermost'), (b'irc', 'IRC')])), ('notification_configuration', jsonfield.fields.JSONField(default=dict)), ('created_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)), ('modified_by', models.ForeignKey(related_name="{u'class': 'notificationtemplate', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)), diff --git a/awx/main/migrations/_reencrypt.py b/awx/main/migrations/_reencrypt.py index c4e502f9a2..84b95e8160 100644 --- a/awx/main/migrations/_reencrypt.py +++ b/awx/main/migrations/_reencrypt.py @@ -12,6 +12,7 @@ from awx.main.notifications.slack_backend import SlackBackend from awx.main.notifications.twilio_backend import TwilioBackend from awx.main.notifications.pagerduty_backend import PagerDutyBackend from awx.main.notifications.hipchat_backend import HipChatBackend +from awx.main.notifications.mattermost_backend import MattermostBackend from awx.main.notifications.webhook_backend import WebhookBackend from awx.main.notifications.irc_backend import IrcBackend @@ -25,6 +26,7 @@ NOTIFICATION_TYPES = [('email', _('Email'), CustomEmailBackend), ('twilio', _('Twilio'), TwilioBackend), ('pagerduty', _('Pagerduty'), PagerDutyBackend), ('hipchat', _('HipChat'), HipChatBackend), + ('mattermost', _('Mattermost'), MattermostBackend), ('webhook', _('Webhook'), WebhookBackend), ('irc', _('IRC'), IrcBackend)] diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 3d81f36906..db445f1852 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -19,6 +19,7 @@ from awx.main.notifications.twilio_backend import TwilioBackend from awx.main.notifications.pagerduty_backend import PagerDutyBackend from awx.main.notifications.hipchat_backend import HipChatBackend from awx.main.notifications.webhook_backend import WebhookBackend +from awx.main.notifications.mattermost_backend import MattermostBackend from awx.main.notifications.irc_backend import IrcBackend from awx.main.fields import JSONField @@ -36,6 +37,7 @@ class NotificationTemplate(CommonModelNameNotUnique): ('pagerduty', _('Pagerduty'), PagerDutyBackend), ('hipchat', _('HipChat'), HipChatBackend), ('webhook', _('Webhook'), WebhookBackend), + ('mattermost', _('Mattermost'), MattermostBackend), ('irc', _('IRC'), IrcBackend)] NOTIFICATION_TYPE_CHOICES = [(x[0], x[1]) for x in NOTIFICATION_TYPES] CLASS_FOR_NOTIFICATION_TYPE = dict([(x[0], x[2]) for x in NOTIFICATION_TYPES]) diff --git a/awx/main/notifications/mattermost_backend.py b/awx/main/notifications/mattermost_backend.py new file mode 100644 index 0000000000..650848c78c --- /dev/null +++ b/awx/main/notifications/mattermost_backend.py @@ -0,0 +1,52 @@ +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved. + +import logging +import requests +import json + +from django.utils.encoding import smart_text +from django.utils.translation import ugettext_lazy as _ +from awx.main.notifications.base import AWXBaseEmailBackend + +logger = logging.getLogger('awx.main.notifications.mattermost_backend') + + +class MattermostBackend(AWXBaseEmailBackend): + + init_parameters = {"mattermost_url": {"label": "Target URL", "type": "string"}, + "mattermost_no_verify_ssl": {"label": "Verify SSL", "type": "bool"}} + recipient_parameter = "mattermost_url" + sender_parameter = None + + def __init__(self, mattermost_no_verify_ssl=False, mattermost_channel=None, mattermost_username=None, + mattermost_icon_url=None, fail_silently=False, **kwargs): + super(MattermostBackend, self).__init__(fail_silently=fail_silently) + self.mattermost_channel = mattermost_channel + self.mattermost_username = mattermost_username + self.mattermost_icon_url = mattermost_icon_url + self.mattermost_no_verify_ssl = mattermost_no_verify_ssl + + def format_body(self, body): + return body + + def send_messages(self, messages): + sent_messages = 0 + for m in messages: + payload = {} + for opt, optval in {'mattermost_icon_url':'icon_url', + 'mattermost_channel': 'channel', 'mattermost_username': 'username'}.iteritems(): + optvalue = getattr(self, opt) + if optvalue is not None: + payload[optval] = optvalue.strip() + + payload['text'] = m.subject + + r = requests.post("{}".format(m.recipients()[0]), + data=json.dumps(payload), verify=(not self.mattermost_no_verify_ssl)) + if r.status_code >= 400: + logger.error(smart_text(_("Error sending notification mattermost: {}").format(r.text))) + if not self.fail_silently: + raise Exception(smart_text(_("Error sending notification mattermost: {}").format(r.text))) + sent_messages += 1 + return sent_messages diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js index 68847d4eda..079f94969c 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.form.js +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -333,6 +333,45 @@ export default ['i18n', function(i18n) { subForm: 'typeSubForm', ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' }, + mattermost_url: { + label: i18n._('Target URL'), + type: 'text', + awRequiredWhen: { + reqExpression: "mattermost_required", + init: "false" + }, + ngShow: "notification_type.value == 'mattermost' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + mattermost_username: { + label: i18n._('Username'), + type: 'text', + ngShow: "notification_type.value == 'mattermost' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + mattermost_channel: { + label: i18n._('Channel'), + type: 'text', + ngShow: "notification_type.value == 'mattermost' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + mattermost_icon_url: { + label: i18n._('Icon URL'), + type: 'text', + ngShow: "notification_type.value == 'mattermost' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + mattermost_no_verify_ssl: { + label: i18n._('Disable SSL Verification'), + type: 'checkbox', + ngShow: "notification_type.value == 'mattermost' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, server: { label: i18n._('IRC Server Address'), type: 'text', diff --git a/awx/ui/client/src/notifications/shared/type-change.service.js b/awx/ui/client/src/notifications/shared/type-change.service.js index bb1b6f0bed..543300b877 100644 --- a/awx/ui/client/src/notifications/shared/type-change.service.js +++ b/awx/ui/client/src/notifications/shared/type-change.service.js @@ -17,6 +17,7 @@ function (i18n) { obj.irc_required = false; obj.twilio_required = false; obj.webhook_required = false; + obj.mattermost_required = false; obj.token_required = false; obj.port_required = false; obj.password_required = false; @@ -48,6 +49,9 @@ function (i18n) { case 'webhook': obj.webhook_required = true; break; + case 'mattermost': + obj.mattermost_required = true; + break; case 'pagerduty': obj.tokenLabel = ' ' + i18n._('API Token'); obj.pagerduty_required = true; diff --git a/docs/notification_system.md b/docs/notification_system.md index 5b4bb6e768..43942db548 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -35,6 +35,7 @@ The currently defined Notification Types are: * Email * Slack * Hipchat +* Mattermost * Pagerduty * Twilio * IRC @@ -99,6 +100,32 @@ Hipchat allows you to create a team with limited users and message history for f https://www.hipchat.com/server +## Mattermost + +The mattermost notification integration uses Incoming Webhooks. A password is not required because the webhook URL itself is the secret. Webhooks must be enabled in the System Console of Mattermost. If the user wishes to allow Ansible Tower notifications to modify the Icon URL and username of the notification then they must enabled these options as well. + +In order to enable these settings in Mattermost: +1. First go to System Console > Integrations > Custom Integrations. Check Enable Incoming Webhooks +2. Optionally, go to System Console > Integrations > Custom Integrations. Check "Enable integrations to override usernames" and Check "Enable integrations to override profile picture icons" +3. Go to Main Menu > Integrations > Incoming Webhook. Click "Add Incoming Webhook" +4. Choose a "Display Name", "Description", and Channel. This channel will be overridden if the notification uses the `channel` option + +* `url`: The incoming webhook URL that was configured in Mattermost. Notifications will use this URL to POST. +* `username`: Optional. The username to display for the notification. +* `channel`: Optional. Override the channel to display the notification in. Mattermost incoming webhooks are tied to a channel by default, so if left blank then this will use the incoming webhook channel. Note, if the channel does not exist then the notification will error out. +* `icon_url`: Optional. A URL pointing to an icon to use for the notification. + +### Testing considerations + +* Make sure all options behave as expected +* Test that all notification options are obeyed +* Test formatting and appearance. Mattermost will use the minimal version of the notification. + +### Test Service + +* Utilize an existing Mattermost installation or use their docker container here: `docker run --name mattermost-preview -d --publish 8065:8065 mattermost/mattermost-preview` +* Turn on Incoming Webhooks and optionally allow Integrations to override usernames and icons in the System Console. + ## Pagerduty Pager duty is a fairly straightforward integration. The user will create an API Key in the pagerduty system (this will be the token that is given to Tower) and then create a "Service" which will provide an "Integration Key" that will be given to Tower also. The other options of note are: