diff options
author | Sebastian <werbungs-newsletter@posteo.de> | 2019-01-20 13:51:23 +0100 |
---|---|---|
committer | Sebastian <werbungs-newsletter@posteo.de> | 2019-01-20 13:51:23 +0100 |
commit | 4058d18593fff598d9f4b9716ea761b3be2719fd (patch) | |
tree | 168e9ac1199b2c3a7d969bf92b017fdbc908d772 | |
parent | Merge pull request #3027 from rooftopcellist/amend_auth_code_help_txt (diff) | |
download | awx-4058d18593fff598d9f4b9716ea761b3be2719fd.tar.xz awx-4058d18593fff598d9f4b9716ea761b3be2719fd.zip |
Add grafana notification type
-rw-r--r-- | awx/main/migrations/0055_v340_add_grafana_notification.py | 25 | ||||
-rw-r--r-- | awx/main/models/notifications.py | 2 | ||||
-rw-r--r-- | awx/main/notifications/grafana_backend.py | 66 | ||||
-rw-r--r-- | awx/ui/client/src/notifications/add/add.controller.js | 6 | ||||
-rw-r--r-- | awx/ui/client/src/notifications/edit/edit.controller.js | 6 | ||||
-rw-r--r-- | awx/ui/client/src/notifications/notificationTemplates.form.js | 63 | ||||
-rw-r--r-- | awx/ui/client/src/notifications/shared/type-change.service.js | 4 | ||||
-rw-r--r-- | docs/notification_system.md | 32 |
8 files changed, 201 insertions, 3 deletions
diff --git a/awx/main/migrations/0055_v340_add_grafana_notification.py b/awx/main/migrations/0055_v340_add_grafana_notification.py new file mode 100644 index 0000000000..bac07a7438 --- /dev/null +++ b/awx/main/migrations/0055_v340_add_grafana_notification.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-01-20 12:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0054_v340_workflow_convergence'), + ] + + operations = [ + migrations.AlterField( + model_name='notification', + name='notification_type', + field=models.CharField(choices=[('email', 'Email'), ('slack', 'Slack'), ('twilio', 'Twilio'), ('pagerduty', 'Pagerduty'), ('grafana', 'Grafana'), ('hipchat', 'HipChat'), ('webhook', 'Webhook'), ('mattermost', 'Mattermost'), ('rocketchat', 'Rocket.Chat'), ('irc', 'IRC')], max_length=32), + ), + migrations.AlterField( + model_name='notificationtemplate', + name='notification_type', + field=models.CharField(choices=[('email', 'Email'), ('slack', 'Slack'), ('twilio', 'Twilio'), ('pagerduty', 'Pagerduty'), ('grafana', 'Grafana'), ('hipchat', 'HipChat'), ('webhook', 'Webhook'), ('mattermost', 'Mattermost'), ('rocketchat', 'Rocket.Chat'), ('irc', 'IRC')], max_length=32), + ), + ] diff --git a/awx/main/models/notifications.py b/awx/main/models/notifications.py index 58ae9fcc3d..02c31b5870 100644 --- a/awx/main/models/notifications.py +++ b/awx/main/models/notifications.py @@ -20,6 +20,7 @@ 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.grafana_backend import GrafanaBackend from awx.main.notifications.rocketchat_backend import RocketChatBackend from awx.main.notifications.irc_backend import IrcBackend from awx.main.fields import JSONField @@ -36,6 +37,7 @@ class NotificationTemplate(CommonModelNameNotUnique): ('slack', _('Slack'), SlackBackend), ('twilio', _('Twilio'), TwilioBackend), ('pagerduty', _('Pagerduty'), PagerDutyBackend), + ('grafana', _('Grafana'), GrafanaBackend), ('hipchat', _('HipChat'), HipChatBackend), ('webhook', _('Webhook'), WebhookBackend), ('mattermost', _('Mattermost'), MattermostBackend), diff --git a/awx/main/notifications/grafana_backend.py b/awx/main/notifications/grafana_backend.py new file mode 100644 index 0000000000..0044a1b098 --- /dev/null +++ b/awx/main/notifications/grafana_backend.py @@ -0,0 +1,66 @@ +# Copyright (c) 2016 Ansible, Inc. +# All Rights Reserved. + +import datetime +import logging +import requests +import dateutil.parser as dp + +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.grafana_backend') + + +class GrafanaBackend(AWXBaseEmailBackend): + + init_parameters = {"grafana_url": {"label": "Grafana URL", "type": "string"}, + "grafana_key": {"label": "Grafana API Key", "type": "password"}} + recipient_parameter = "grafana_url" + sender_parameter = None + + def __init__(self, grafana_key,dashboardId=None, panelId=None, annotation_tags=None, grafana_no_verify_ssl=False, isRegion=True, + fail_silently=False, **kwargs): + super(GrafanaBackend, self).__init__(fail_silently=fail_silently) + self.grafana_key = grafana_key + self.dashboardId = dashboardId + self.panelId = panelId + self.annotation_tags = annotation_tags if annotation_tags is not None else [] + self.grafana_no_verify_ssl = grafana_no_verify_ssl + self.isRegion = isRegion + + def format_body(self, body): + return body + + def send_messages(self, messages): + sent_messages = 0 + for m in messages: + grafana_data = {} + grafana_headers = {} + try: + epoch=datetime.datetime.utcfromtimestamp(0) + grafana_data['time'] = int((dp.parse(m.body['started']).replace(tzinfo=None) - epoch).total_seconds() * 1000) + grafana_data['timeEnd'] = int((dp.parse(m.body['finished']).replace(tzinfo=None) - epoch).total_seconds() * 1000) + except ValueError: + logger.error(smart_text(_("Error converting time {} or timeEnd {} to int.").format(m.body['started'],m.body['finished']))) + if not self.fail_silently: + raise Exception(smart_text(_("Error converting time {} and/or timeEnd {} to int.").format(m.body['started'],m.body['finished']))) + grafana_data['isRegion'] = self.isRegion + grafana_data['dashboardId'] = self.dashboardId + grafana_data['panelId'] = self.panelId + grafana_data['tags'] = self.annotation_tags + grafana_data['text'] = m.subject + grafana_headers['Authorization'] = "Bearer {}".format(self.grafana_key) + grafana_headers['Content-Type'] = "application/json" + r = requests.post("{}/api/annotations".format(m.recipients()[0]), + json=grafana_data, + headers=grafana_headers, + verify=(not self.grafana_no_verify_ssl)) + if r.status_code >= 400: + logger.error(smart_text(_("Error sending notification grafana: {}").format(r.text))) + if not self.fail_silently: + raise Exception(smart_text(_("Error sending notification grafana: {}").format(r.text))) + sent_messages += 1 + return sent_messages diff --git a/awx/ui/client/src/notifications/add/add.controller.js b/awx/ui/client/src/notifications/add/add.controller.js index 3ada22743e..8bb236eb06 100644 --- a/awx/ui/client/src/notifications/add/add.controller.js +++ b/awx/ui/client/src/notifications/add/add.controller.js @@ -186,7 +186,11 @@ export default ['Rest', 'Wait', 'NotificationsFormObject', if (field.type === 'textarea') { if (field.name === 'headers') { $scope[i] = JSON.parse($scope[i]); - } else { + } + else if (field.name === 'annotation_tags' && $scope.notification_type.value === "grafana" && value === null) { + $scope[i] = null; + } + else { $scope[i] = $scope[i].toString().split('\n'); } } diff --git a/awx/ui/client/src/notifications/edit/edit.controller.js b/awx/ui/client/src/notifications/edit/edit.controller.js index 561ab09e58..e49b95dd3c 100644 --- a/awx/ui/client/src/notifications/edit/edit.controller.js +++ b/awx/ui/client/src/notifications/edit/edit.controller.js @@ -256,7 +256,11 @@ export default ['Rest', 'Wait', if (field.type === 'textarea') { if (field.name === 'headers') { $scope[i] = JSON.parse($scope[i]); - } else { + } + else if (field.name === 'annotation_tags' && $scope.notification_type.value === "grafana" && value === null) { + $scope[i] = null; + } + else { $scope[i] = $scope[i].toString().split('\n'); } } diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js index abe1c5fd3b..87bfd8c457 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.form.js +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -261,6 +261,69 @@ export default ['i18n', function(i18n) { subForm: 'typeSubForm', ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' }, + grafana_url: { + label: i18n._('Grafana URL'), + type: 'text', + awPopOver: i18n._('The base URL of the Grafana server - the /api/annotations endpoint will be added automatically to the base Grafana URL.'), + placeholder: 'https://grafana.com', + dataPlacement: 'right', + dataContainer: "body", + awRequiredWhen: { + reqExpression: "grafana_required", + init: "false" + }, + ngShow: "notification_type.value == 'grafana' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + grafana_key: { + label: i18n._('Grafana API Key'), + type: 'sensitive', + hasShowInputButton: true, + name: 'grafana_key', + awRequiredWhen: { + reqExpression: "grafana_required", + init: "false" + }, + ngShow: "notification_type.value == 'grafana' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + dashboardId: { + label: i18n._('ID of the Dashboard (optional)'), + type: 'number', + integer: true, + ngShow: "notification_type.value == 'grafana' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + panelId: { + label: i18n._('ID of the Panel (optional)'), + type: 'number', + integer: true, + ngShow: "notification_type.value == 'grafana' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + annotation_tags: { + label: i18n._('Tags for the Annotation (optional)'), + dataTitle: i18n._('Tags for the Annotation'), + type: 'textarea', + name: 'annotation_tags', + rows: 3, + placeholder: 'ansible', + awPopOver: i18n._('Enter one Annotation Tag per line, without commas.'), + ngShow: "notification_type.value == 'grafana' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, + grafana_no_verify_ssl: { + label: i18n._('Disable SSL Verification'), + type: 'checkbox', + ngShow: "notification_type.value == 'grafana' ", + subForm: 'typeSubForm', + ngDisabled: '!(notification_template.summary_fields.user_capabilities.edit || canAdd)' + }, api_url: { label: 'API URL', 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 997902959a..5695a4ca10 100644 --- a/awx/ui/client/src/notifications/shared/type-change.service.js +++ b/awx/ui/client/src/notifications/shared/type-change.service.js @@ -12,6 +12,7 @@ function (i18n) { obj.email_required = false; obj.slack_required = false; + obj.grafana_required = false; obj.hipchat_required = false; obj.pagerduty_required = false; obj.irc_required = false; @@ -38,6 +39,9 @@ function (i18n) { obj.token_required = true; obj.channel_required = true; break; + case 'grafana': + obj.grafana_required = true; + break; case 'hipchat': obj.tokenLabel = ' ' + i18n._('Token'); obj.hipchat_required = true; diff --git a/docs/notification_system.md b/docs/notification_system.md index b2fcd0d662..7418854f76 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -41,6 +41,7 @@ The currently defined Notification Types are: * Twilio * IRC * Webhook +* Grafana Each of these have their own configuration and behavioral semantics and testing them may need to be approached in different ways. The following sections will give as much detail as possible. @@ -125,7 +126,7 @@ In order to enable these settings in Mattermost: ### 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. +* Turn on Incoming Webhooks and optionally allow Integrations to override usernames and icons in the System Console. ## Rocket.Chat @@ -231,3 +232,32 @@ Note that this won't respond correctly to the notification so it will yield an e https://gist.github.com/matburt/73bfbf85c2443f39d272 This demonstrates how to define an endpoint and parse headers and json content, it doesn't show configuring Flask for HTTPS but this is also pretty straightforward: http://flask.pocoo.org/snippets/111/ + + +## Grafana + +The Grafana notification type allows you to create Grafana annotations, Details about this feature of Grafana are available at http://docs.grafana.org/reference/annotations/. In order to allow Tower to add annotations an API Key needs to be created in Grafana. Note that the created annotations are region events with start and endtime of the associated Tower Job. The annotation description is also provided by the subject of the associated Tower Job, e.g.: +``` +Job #1 'Ping Macbook' succeeded: https://towerhost/#/jobs/playbook/1 +``` + +The configurable options of the Grafana notification type are: +* `Grafana URL`: The base URL of the Grafana server (required). **Note**: the /api/annotations endpoint will be added automatically to the base Grafana URL. +* `API Key`: The Grafana API Key to authenticate (required) +* `ID of the Dashboard`: To create annotations in a specific Grafana dashboard enter the ID of the dashboard (optional). +* `ID of the Panel`: To create annotations in a specific Panel enter the ID of the panel (optional). +**Note**: If neither dashboardId nor panelId are provided then a global annotation is created and can be queried in any dashboard that adds the Grafana annotations data source. +* `Annotations tags`: List of tags to add to the annotation. One tag per line. +* `Disable SSL Verification`: Disable the verification of the ssl certificate, e.g. when using a self-signed SSL certificate for Grafana. + +### Test Considerations + +* Make sure that all options behave as expected +* Test that all notification options are obeyed +* e.g. Make sure the annotation gets created on the desired dashboard and/or panel and with the configured tags + +### Test Service +* Utilize an existing Grafana installation or use their docker containers from http://docs.grafana.org/installation/docker/ +* Create an API Key in the Grafana configuration settings +* (Optional) Lookup dashboardId and/or panelId if needed +* (Optional) define tags for the annotation |