# -*- coding: utf-8 -*-
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see .
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: grafana_annotations
type: notification
short_description: send ansible events as annotations on charts to grafana over http api.
author: "RĂ©mi REY (@rrey)"
description:
- This callback will report start, failed and stats events to Grafana as annotations (https://grafana.com)
requirements:
- whitelisting in configuration
options:
grafana_url:
description: Grafana annotations api URL
required: True
env:
- name: GRAFANA_URL
ini:
- section: callback_grafana_annotations
key: grafana_url
type: string
validate_certs:
description: validate the SSL certificate of the Grafana server. (For HTTPS url)
env:
- name: GRAFANA_VALIDATE_CERT
ini:
- section: callback_grafana_annotations
key: validate_grafana_certs
- section: callback_grafana_annotations
key: validate_certs
default: True
type: bool
aliases: [ validate_grafana_certs ]
http_agent:
description: The HTTP 'User-agent' value to set in HTTP requets.
env:
- name: HTTP_AGENT
ini:
- section: callback_grafana_annotations
key: http_agent
default: 'Ansible (grafana_annotations callback)'
type: string
grafana_api_key:
description: Grafana API key, allowing to authenticate when posting on the HTTP API.
If not provided, grafana_login and grafana_password will
be required.
env:
- name: GRAFANA_API_KEY
ini:
- section: callback_grafana_annotations
key: grafana_api_key
type: string
grafana_user:
description: Grafana user used for authentication. Ignored if grafana_api_key is provided.
env:
- name: GRAFANA_USER
ini:
- section: callback_grafana_annotations
key: grafana_user
default: ansible
type: string
grafana_password:
description: Grafana password used for authentication. Ignored if grafana_api_key is provided.
env:
- name: GRAFANA_PASSWORD
ini:
- section: callback_grafana_annotations
key: grafana_password
default: ansible
type: string
grafana_dashboard_id:
description: The grafana dashboard id where the annotation shall be created.
env:
- name: GRAFANA_DASHBOARD_ID
ini:
- section: callback_grafana_annotations
key: grafana_dashboard_id
type: integer
grafana_panel_ids:
description: The grafana panel ids where the annotation shall be created.
Give a single integer or a comma-separated list of integers.
env:
- name: GRAFANA_PANEL_IDS
ini:
- section: callback_grafana_annotations
key: grafana_panel_ids
default: []
type: list
'''
import json
import socket
import getpass
from datetime import datetime
from ansible.module_utils._text import to_text
from ansible.module_utils.urls import open_url
from ansible.plugins.callback import CallbackBase
PLAYBOOK_START_TXT = """\
Started playbook {playbook}
From '{hostname}'
By user '{username}'
"""
PLAYBOOK_ERROR_TXT = """\
Playbook {playbook} Failure !
From '{hostname}'
By user '{username}'
'{task}' failed on {host}
debug: {result}
"""
PLAYBOOK_STATS_TXT = """\
Playbook {playbook}
Duration: {duration}
Status: {status}
From '{hostname}'
By user '{username}'
Result:
{summary}
"""
def to_millis(dt):
return int(dt.strftime('%s')) * 1000
class CallbackModule(CallbackBase):
"""
ansible grafana callback plugin
ansible.cfg:
callback_plugins =
callback_whitelist = grafana_annotations
and put the plugin in
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.grafana.grafana_annotations'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
self.headers = {'Content-Type': 'application/json'}
self.force_basic_auth = False
self.hostname = socket.gethostname()
self.username = getpass.getuser()
self.start_time = datetime.now()
self.errors = 0
def set_options(self, task_keys=None, var_options=None, direct=None):
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.grafana_api_key = self.get_option('grafana_api_key')
self.grafana_url = self.get_option('grafana_url')
self.validate_grafana_certs = self.get_option('validate_certs')
self.http_agent = self.get_option('http_agent')
self.grafana_user = self.get_option('grafana_user')
self.grafana_password = self.get_option('grafana_password')
self.dashboard_id = self.get_option('grafana_dashboard_id')
self.panel_ids = self.get_option('grafana_panel_ids')
if self.grafana_api_key:
self.headers['Authorization'] = "Bearer %s" % self.grafana_api_key
else:
self.force_basic_auth = True
if self.grafana_url is None:
self.disabled = True
self._display.warning('Grafana URL was not provided. The '
'Grafana URL can be provided using '
'the `GRAFANA_URL` environment variable.')
self._display.debug('Grafana URL: %s' % self.grafana_url)
def v2_playbook_on_start(self, playbook):
self.playbook = playbook._file_name
text = PLAYBOOK_START_TXT.format(playbook=self.playbook, hostname=self.hostname,
username=self.username)
data = {
'time': to_millis(self.start_time),
'text': text,
'tags': ['ansible', 'ansible_event_start', self.playbook, self.hostname]
}
self._send_annotation(data)
def v2_playbook_on_stats(self, stats):
end_time = datetime.now()
duration = end_time - self.start_time
summarize_stat = {}
for host in stats.processed.keys():
summarize_stat[host] = stats.summarize(host)
status = "FAILED"
if self.errors == 0:
status = "OK"
text = PLAYBOOK_STATS_TXT.format(playbook=self.playbook, hostname=self.hostname,
duration=duration.total_seconds(),
status=status, username=self.username,
summary=json.dumps(summarize_stat))
data = {
'time': to_millis(self.start_time),
'timeEnd': to_millis(end_time),
'isRegion': True,
'text': text,
'tags': ['ansible', 'ansible_report', self.playbook, self.hostname]
}
self._send_annotations(data)
def v2_runner_on_failed(self, result, ignore_errors=False, **kwargs):
text = PLAYBOOK_ERROR_TXT.format(playbook=self.playbook, hostname=self.hostname,
username=self.username, task=result._task,
host=result._host.name, result=self._dump_results(result._result))
if ignore_errors:
return
data = {
'time': to_millis(datetime.now()),
'text': text,
'tags': ['ansible', 'ansible_event_failure', self.playbook, self.hostname]
}
self.errors += 1
self._send_annotations(data)
def _send_annotations(self, data):
if self.dashboard_id:
data["dashboardId"] = int(self.dashboard_id)
if self.panel_ids:
for panel_id in self.panel_ids:
data["panelId"] = int(panel_id)
self._send_annotation(data)
else:
self._send_annotation(data)
def _send_annotation(self, annotation):
try:
open_url(self.grafana_url, data=json.dumps(annotation), headers=self.headers,
method="POST",
validate_certs=self.validate_grafana_certs,
url_username=self.grafana_user, url_password=self.grafana_password,
http_agent=self.http_agent, force_basic_auth=self.force_basic_auth)
except Exception as e:
self._display.error(u'Could not submit message to Grafana: %s' % to_text(e))