Merge pull request #2959 from crab86/devel

Add Grafana notification type

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot] 2019-01-21 17:21:14 +00:00 committed by GitHub
commit d021c253aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 314 additions and 3 deletions

View File

@ -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),
),
]

View File

@ -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),

View File

@ -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

View File

@ -0,0 +1,113 @@
from unittest import mock
import datetime as dt
from django.core.mail.message import EmailMessage
import awx.main.notifications.grafana_backend as grafana_backend
def test_send_messages():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
m={}
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey")
message = EmailMessage(m['subject'],{"started":m['started'],"finished":m['finished']}, [], ['https://example.com', ])
sent_messages = backend.send_messages([message, ])
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'tags': [], 'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': None, 'time': 60000, 'dashboardId': None},
verify=True)
assert sent_messages == 1
def test_send_messages_with_no_verify_ssl():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
m={}
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey",grafana_no_verify_ssl=True)
message = EmailMessage(m['subject'],{"started":m['started'],"finished":m['finished']}, [], ['https://example.com', ])
sent_messages = backend.send_messages([message, ])
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'tags': [], 'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': None,'time': 60000, 'dashboardId': None},
verify=False)
assert sent_messages == 1
def test_send_messages_with_dashboardid():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
m={}
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey",dashboardId=42)
message = EmailMessage(m['subject'],{"started":m['started'],"finished":m['finished']}, [], ['https://example.com', ])
sent_messages = backend.send_messages([message, ])
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'tags': [], 'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': None, 'time': 60000, 'dashboardId': 42},
verify=True)
assert sent_messages == 1
def test_send_messages_with_panelid():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
m={}
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey",dashboardId=None,panelId=42)
message = EmailMessage(m['subject'],{"started":m['started'],"finished":m['finished']}, [], ['https://example.com', ])
sent_messages = backend.send_messages([message, ])
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'tags': [], 'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': 42, 'time': 60000, 'dashboardId': None},
verify=True)
assert sent_messages == 1
def test_send_messages_with_bothids():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
m={}
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey",dashboardId=42,panelId=42)
message = EmailMessage(m['subject'],{"started":m['started'],"finished":m['finished']}, [], ['https://example.com', ])
sent_messages = backend.send_messages([message, ])
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'tags': [], 'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': 42, 'time': 60000, 'dashboardId': 42},
verify=True)
assert sent_messages == 1
def test_send_messages_with_tags():
with mock.patch('awx.main.notifications.grafana_backend.requests') as requests_mock:
requests_mock.post.return_value.status_code = 200
m={}
m['started'] = dt.datetime.utcfromtimestamp(60).isoformat()
m['finished'] = dt.datetime.utcfromtimestamp(120).isoformat()
m['subject'] = "test subject"
backend = grafana_backend.GrafanaBackend("testapikey",dashboardId=None,panelId=None,annotation_tags=["ansible"])
message = EmailMessage(m['subject'],{"started":m['started'],"finished":m['finished']}, [], ['https://example.com', ])
sent_messages = backend.send_messages([message, ])
requests_mock.post.assert_called_once_with(
'https://example.com/api/annotations',
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'},
json={'tags': ['ansible'], 'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': None, 'time': 60000, 'dashboardId': None},
verify=True)
assert sent_messages == 1

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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',

View File

@ -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;

View File

@ -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