mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
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:
commit
d021c253aa
25
awx/main/migrations/0055_v340_add_grafana_notification.py
Normal file
25
awx/main/migrations/0055_v340_add_grafana_notification.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
|
||||
66
awx/main/notifications/grafana_backend.py
Normal file
66
awx/main/notifications/grafana_backend.py
Normal 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
|
||||
113
awx/main/tests/unit/notifications/test_grafana.py
Normal file
113
awx/main/tests/unit/notifications/test_grafana.py
Normal 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
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user