summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastian <werbungs-newsletter@posteo.de>2019-01-20 13:51:23 +0100
committerSebastian <werbungs-newsletter@posteo.de>2019-01-20 13:51:23 +0100
commit4058d18593fff598d9f4b9716ea761b3be2719fd (patch)
tree168e9ac1199b2c3a7d969bf92b017fdbc908d772
parentMerge pull request #3027 from rooftopcellist/amend_auth_code_help_txt (diff)
downloadawx-4058d18593fff598d9f4b9716ea761b3be2719fd.tar.xz
awx-4058d18593fff598d9f4b9716ea761b3be2719fd.zip
Add grafana notification type
-rw-r--r--awx/main/migrations/0055_v340_add_grafana_notification.py25
-rw-r--r--awx/main/models/notifications.py2
-rw-r--r--awx/main/notifications/grafana_backend.py66
-rw-r--r--awx/ui/client/src/notifications/add/add.controller.js6
-rw-r--r--awx/ui/client/src/notifications/edit/edit.controller.js6
-rw-r--r--awx/ui/client/src/notifications/notificationTemplates.form.js63
-rw-r--r--awx/ui/client/src/notifications/shared/type-change.service.js4
-rw-r--r--docs/notification_system.md32
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