Migrated to community.grafana

pull/67091/head
Ansible Core Team 4 years ago committed by Matt Martz
parent 4e488d8435
commit 43f47d553e

@ -1,556 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Thierry Sallé (@seuf)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.1'
}
DOCUMENTATION = '''
---
module: grafana_dashboard
author:
- Thierry Sallé (@seuf)
version_added: "2.5"
short_description: Manage Grafana dashboards
description:
- Create, update, delete, export Grafana dashboards via API.
options:
url:
description:
- The Grafana URL.
required: true
aliases: [ grafana_url ]
version_added: 2.7
url_username:
description:
- The Grafana API user.
default: admin
aliases: [ grafana_user ]
version_added: 2.7
url_password:
description:
- The Grafana API password.
default: admin
aliases: [ grafana_password ]
version_added: 2.7
grafana_api_key:
description:
- The Grafana API key.
- If set, I(grafana_user) and I(grafana_password) will be ignored.
org_id:
description:
- The Grafana Organisation ID where the dashboard will be imported / exported.
- Not used when I(grafana_api_key) is set, because the grafana_api_key only belongs to one organisation..
default: 1
folder:
description:
- The Grafana folder where this dashboard will be imported to.
default: General
version_added: '2.10'
state:
description:
- State of the dashboard.
required: true
choices: [ absent, export, present ]
default: present
slug:
description:
- Deprecated since Grafana 5. Use grafana dashboard uid instead.
- slug of the dashboard. It's the friendly url name of the dashboard.
- When C(state) is C(present), this parameter can override the slug in the meta section of the json file.
- If you want to import a json dashboard exported directly from the interface (not from the api),
you have to specify the slug parameter because there is no meta section in the exported json.
uid:
version_added: 2.7
description:
- uid of the dashboard to export when C(state) is C(export) or C(absent).
path:
description:
- The path to the json file containing the Grafana dashboard to import or export.
overwrite:
description:
- Override existing dashboard when state is present.
type: bool
default: 'no'
commit_message:
description:
- Set a commit message for the version history.
- Only used when C(state) is C(present).
- C(message) alias is deprecated in Ansible 2.10, since it is used internally by Ansible Core Engine.
aliases: [ 'message' ]
validate_certs:
description:
- If C(no), SSL certificates will not be validated.
- This should only be used on personally controlled sites using self-signed certificates.
type: bool
default: 'yes'
client_cert:
description:
- PEM formatted certificate chain file to be used for SSL client authentication.
- This file can also include the key as well, and if the key is included, client_key is not required
version_added: 2.7
client_key:
description:
- PEM formatted file that contains your private key to be used for SSL client
- authentication. If client_cert contains both the certificate and key, this option is not required
version_added: 2.7
use_proxy:
description:
- Boolean of whether or not to use proxy.
default: 'yes'
type: bool
version_added: 2.7
'''
EXAMPLES = '''
- hosts: localhost
connection: local
tasks:
- name: Import Grafana dashboard foo
grafana_dashboard:
grafana_url: http://grafana.company.com
grafana_api_key: "{{ grafana_api_key }}"
state: present
commit_message: Updated by ansible
overwrite: yes
path: /path/to/dashboards/foo.json
- name: Export dashboard
grafana_dashboard:
grafana_url: http://grafana.company.com
grafana_user: "admin"
grafana_password: "{{ grafana_password }}"
org_id: 1
state: export
uid: "000000653"
path: "/path/to/dashboards/000000653.json"
'''
RETURN = '''
---
uid:
description: uid or slug of the created / deleted / exported dashboard.
returned: success
type: str
sample: 000000063
'''
import json
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url, url_argument_spec
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils._text import to_native
from ansible.module_utils._text import to_text
__metaclass__ = type
class GrafanaAPIException(Exception):
pass
class GrafanaMalformedJson(Exception):
pass
class GrafanaExportException(Exception):
pass
class GrafanaDeleteException(Exception):
pass
def grafana_switch_organisation(module, grafana_url, org_id, headers):
r, info = fetch_url(module, '%s/api/user/using/%s' % (grafana_url, org_id), headers=headers, method='POST')
if info['status'] != 200:
raise GrafanaAPIException('Unable to switch to organization %s : %s' % (org_id, info))
def grafana_headers(module, data):
headers = {'content-type': 'application/json; charset=utf8'}
if 'grafana_api_key' in data and data['grafana_api_key']:
headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
else:
module.params['force_basic_auth'] = True
grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)
return headers
def get_grafana_version(module, grafana_url, headers):
grafana_version = None
r, info = fetch_url(module, '%s/api/frontend/settings' % grafana_url, headers=headers, method='GET')
if info['status'] == 200:
try:
settings = json.loads(to_text(r.read()))
grafana_version = settings['buildInfo']['version'].split('.')[0]
except UnicodeError as e:
raise GrafanaAPIException('Unable to decode version string to Unicode')
except Exception as e:
raise GrafanaAPIException(e)
else:
raise GrafanaAPIException('Unable to get grafana version : %s' % info)
return int(grafana_version)
def grafana_folder_exists(module, grafana_url, folder_name, headers):
# the 'General' folder is a special case, it's ID is always '0'
if folder_name == 'General':
return True, 0
try:
r, info = fetch_url(module, '%s/api/folders' % grafana_url, headers=headers, method='GET')
if info['status'] != 200:
raise GrafanaAPIException("Unable to query Grafana API for folders (name: %s): %d" % (folder_name, info['status']))
folders = json.loads(r.read())
for folder in folders:
if folder['title'] == folder_name:
return True, folder['id']
except Exception as e:
raise GrafanaAPIException(e)
return False, 0
def grafana_dashboard_exists(module, grafana_url, uid, headers):
dashboard_exists = False
dashboard = {}
grafana_version = get_grafana_version(module, grafana_url, headers)
if grafana_version >= 5:
uri = '%s/api/dashboards/uid/%s' % (grafana_url, uid)
else:
uri = '%s/api/dashboards/db/%s' % (grafana_url, uid)
r, info = fetch_url(module, uri, headers=headers, method='GET')
if info['status'] == 200:
dashboard_exists = True
try:
dashboard = json.loads(r.read())
except Exception as e:
raise GrafanaAPIException(e)
elif info['status'] == 404:
dashboard_exists = False
else:
raise GrafanaAPIException('Unable to get dashboard %s : %s' % (uid, info))
return dashboard_exists, dashboard
def grafana_dashboard_search(module, grafana_url, folder_id, title, headers):
# search by title
uri = '%s/api/search?%s' % (grafana_url, urlencode({
'folderIds': folder_id,
'query': title,
'type': 'dash-db'
}))
r, info = fetch_url(module, uri, headers=headers, method='GET')
if info['status'] == 200:
try:
dashboards = json.loads(r.read())
for d in dashboards:
if d['title'] == title:
return grafana_dashboard_exists(module, grafana_url, d['uid'], headers)
except Exception as e:
raise GrafanaAPIException(e)
else:
raise GrafanaAPIException('Unable to search dashboard %s : %s' % (title, info))
return False, None
# for comparison, we sometimes need to ignore a few keys
def grafana_dashboard_changed(payload, dashboard):
# you don't need to set the version, but '0' is incremented to '1' by Grafana's API
if payload['dashboard']['version'] == 0:
del(payload['dashboard']['version'])
del(dashboard['dashboard']['version'])
# the meta key is not part of the 'payload' ever
if 'meta' in dashboard:
del(dashboard['meta'])
# new dashboards don't require an id attribute (or, it can be 'null'), Grafana's API will generate it
if payload['dashboard']['id'] is None:
del(dashboard['dashboard']['id'])
del(payload['dashboard']['id'])
if payload == dashboard:
return True
return False
def grafana_create_dashboard(module, data):
# define data payload for grafana API
try:
with open(data['path'], 'r') as json_file:
payload = json.load(json_file)
except Exception as e:
raise GrafanaAPIException("Can't load json file %s" % to_native(e))
# Check that the dashboard JSON is nested under the 'dashboard' key
if 'dashboard' not in payload:
payload = {'dashboard': payload}
# define http header
headers = grafana_headers(module, data)
grafana_version = get_grafana_version(module, data['grafana_url'], headers)
if grafana_version < 5:
if data.get('slug'):
uid = data['slug']
elif 'meta' in payload and 'slug' in payload['meta']:
uid = payload['meta']['slug']
else:
raise GrafanaMalformedJson('No slug found in json. Needed with grafana < 5')
else:
if data.get('uid'):
uid = data['uid']
elif 'uid' in payload['dashboard']:
uid = payload['dashboard']['uid']
else:
uid = None
result = {}
# test if the folder exists
if grafana_version >= 5:
folder_exists, folder_id = grafana_folder_exists(module, data['grafana_url'], data['folder'], headers)
if folder_exists is False:
result['msg'] = "Dashboard folder '%s' does not exist." % data['folder']
result['uid'] = uid
result['changed'] = False
return result
payload['folderId'] = folder_id
# test if dashboard already exists
if uid:
dashboard_exists, dashboard = grafana_dashboard_exists(
module, data['grafana_url'], uid, headers=headers)
else:
dashboard_exists, dashboard = grafana_dashboard_search(
module, data['grafana_url'], folder_id, payload['dashboard']['title'], headers=headers)
if dashboard_exists is True:
if grafana_dashboard_changed(payload, dashboard):
# update
if 'overwrite' in data and data['overwrite']:
payload['overwrite'] = True
if 'commit_message' in data and data['commit_message']:
payload['message'] = data['commit_message']
r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'],
data=json.dumps(payload), headers=headers, method='POST')
if info['status'] == 200:
if grafana_version >= 5:
try:
dashboard = json.loads(r.read())
uid = dashboard['uid']
except Exception as e:
raise GrafanaAPIException(e)
result['uid'] = uid
result['msg'] = "Dashboard %s updated" % payload['dashboard']['title']
result['changed'] = True
else:
body = json.loads(info['body'])
raise GrafanaAPIException('Unable to update the dashboard %s : %s (HTTP: %d)' %
(uid, body['commit_message'], info['status']))
else:
# unchanged
result['uid'] = uid
result['msg'] = "Dashboard %s unchanged." % payload['dashboard']['title']
result['changed'] = False
else:
# create
if folder_exists is True:
payload['folderId'] = folder_id
r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'],
data=json.dumps(payload), headers=headers, method='POST')
if info['status'] == 200:
result['msg'] = "Dashboard %s created" % payload['dashboard']['title']
result['changed'] = True
if grafana_version >= 5:
try:
dashboard = json.loads(r.read())
uid = dashboard['uid']
except Exception as e:
raise GrafanaAPIException(e)
result['uid'] = uid
else:
raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' %
(payload['dashboard']['title'], info['status'], info))
return result
def grafana_delete_dashboard(module, data):
# define http headers
headers = grafana_headers(module, data)
grafana_version = get_grafana_version(module, data['grafana_url'], headers)
if grafana_version < 5:
if data.get('slug'):
uid = data['slug']
else:
raise GrafanaMalformedJson('No slug parameter. Needed with grafana < 5')
else:
if data.get('uid'):
uid = data['uid']
else:
raise GrafanaDeleteException('No uid specified %s')
# test if dashboard already exists
dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], uid, headers=headers)
result = {}
if dashboard_exists is True:
# delete
if grafana_version < 5:
r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (data['grafana_url'], uid), headers=headers, method='DELETE')
else:
r, info = fetch_url(module, '%s/api/dashboards/uid/%s' % (data['grafana_url'], uid), headers=headers, method='DELETE')
if info['status'] == 200:
result['msg'] = "Dashboard %s deleted" % uid
result['changed'] = True
result['uid'] = uid
else:
raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (uid, info))
else:
# dashboard does not exist, do nothing
result = {'msg': "Dashboard %s does not exist." % uid,
'changed': False,
'uid': uid}
return result
def grafana_export_dashboard(module, data):
# define http headers
headers = grafana_headers(module, data)
grafana_version = get_grafana_version(module, data['grafana_url'], headers)
if grafana_version < 5:
if data.get('slug'):
uid = data['slug']
else:
raise GrafanaMalformedJson('No slug parameter. Needed with grafana < 5')
else:
if data.get('uid'):
uid = data['uid']
else:
raise GrafanaExportException('No uid specified')
# test if dashboard already exists
dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], uid, headers=headers)
if dashboard_exists is True:
try:
with open(data['path'], 'w') as f:
f.write(json.dumps(dashboard))
except Exception as e:
raise GrafanaExportException("Can't write json file : %s" % to_native(e))
result = {'msg': "Dashboard %s exported to %s" % (uid, data['path']),
'uid': uid,
'changed': True}
else:
result = {'msg': "Dashboard %s does not exist." % uid,
'uid': uid,
'changed': False}
return result
def main():
# use the predefined argument spec for url
argument_spec = url_argument_spec()
# remove unnecessary arguments
del argument_spec['force']
del argument_spec['force_basic_auth']
del argument_spec['http_agent']
argument_spec.update(
state=dict(choices=['present', 'absent', 'export'], default='present'),
url=dict(aliases=['grafana_url'], required=True),
url_username=dict(aliases=['grafana_user'], default='admin'),
url_password=dict(aliases=['grafana_password'], default='admin', no_log=True),
grafana_api_key=dict(type='str', no_log=True),
org_id=dict(default=1, type='int'),
folder=dict(type='str', default='General'),
uid=dict(type='str'),
slug=dict(type='str'),
path=dict(type='str'),
overwrite=dict(type='bool', default=False),
commit_message=dict(type='str', aliases=['message'], deprecated_aliases=[dict(name='message', version='2.14')]),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=[['url_username', 'url_password', 'org_id']],
mutually_exclusive=[['grafana_user', 'grafana_api_key'], ['uid', 'slug']],
)
if 'message' in module.params:
module.fail_json(msg="'message' is reserved keyword, please change this parameter to 'commit_message'")
try:
if module.params['state'] == 'present':
result = grafana_create_dashboard(module, module.params)
elif module.params['state'] == 'absent':
result = grafana_delete_dashboard(module, module.params)
else:
result = grafana_export_dashboard(module, module.params)
except GrafanaAPIException as e:
module.fail_json(
failed=True,
msg="error : %s" % to_native(e)
)
return
except GrafanaMalformedJson as e:
module.fail_json(
failed=True,
msg="error : %s" % to_native(e)
)
return
except GrafanaDeleteException as e:
module.fail_json(
failed=True,
msg="error : Can't delete dashboard : %s" % to_native(e)
)
return
except GrafanaExportException as e:
module.fail_json(
failed=True,
msg="error : Can't export dashboard : %s" % to_native(e)
)
return
module.exit_json(
failed=False,
**result
)
return
if __name__ == '__main__':
main()

@ -1,683 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Thierry Sallé (@seuf)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.1'
}
DOCUMENTATION = '''
---
module: grafana_datasource
author:
- Thierry Sallé (@seuf)
- Martin Wang (@martinwangjian)
version_added: "2.5"
short_description: Manage Grafana datasources
description:
- Create/update/delete Grafana datasources via API.
options:
grafana_url:
description:
- The Grafana URL.
required: true
name:
description:
- The name of the datasource.
required: true
ds_type:
description:
- The type of the datasource.
required: true
choices: [ graphite, prometheus, elasticsearch, influxdb, opentsdb, mysql, postgres, cloudwatch, alexanderzobnin-zabbix-datasource]
url:
description:
- The URL of the datasource.
required: true
aliases: [ ds_url ]
access:
description:
- The access mode for this datasource.
choices: [ direct, proxy ]
default: proxy
url_username:
description:
- The Grafana API user.
default: admin
aliases: [ grafana_user ]
version_added: 2.7
url_password:
description:
- The Grafana API password.
default: admin
aliases: [ grafana_password ]
version_added: 2.7
grafana_api_key:
description:
- The Grafana API key.
- If set, C(grafana_user) and C(grafana_password) will be ignored.
database:
description:
- Name of the database for the datasource.
- This options is required when the C(ds_type) is C(influxdb), C(elasticsearch) (index name), C(mysql) or C(postgres).
required: false
user:
description:
- The datasource login user for influxdb datasources.
password:
description:
- The datasource password
basic_auth_user:
description:
- The datasource basic auth user.
- Setting this option with basic_auth_password will enable basic auth.
basic_auth_password:
description:
- The datasource basic auth password, when C(basic auth) is C(yes).
with_credentials:
description:
- Whether credentials such as cookies or auth headers should be sent with cross-site requests.
type: bool
default: 'no'
tls_client_cert:
description:
- The client TLS certificate.
- If C(tls_client_cert) and C(tls_client_key) are set, this will enable TLS authentication.
- Starts with ----- BEGIN CERTIFICATE -----
tls_client_key:
description:
- The client TLS private key
- Starts with ----- BEGIN RSA PRIVATE KEY -----
tls_ca_cert:
description:
- The TLS CA certificate for self signed certificates.
- Only used when C(tls_client_cert) and C(tls_client_key) are set.
tls_skip_verify:
description:
- Skip the TLS datasource certificate verification.
type: bool
default: False
version_added: 2.6
is_default:
description:
- Make this datasource the default one.
type: bool
default: 'no'
org_id:
description:
- Grafana Organisation ID in which the datasource should be created.
- Not used when C(grafana_api_key) is set, because the C(grafana_api_key) only belong to one organisation.
default: 1
state:
description:
- Status of the datasource
choices: [ absent, present ]
default: present
es_version:
description:
- Elasticsearch version (for C(ds_type = elasticsearch) only)
- Version 56 is for elasticsearch 5.6+ where you can specify the C(max_concurrent_shard_requests) option.
choices: [ 2, 5, 56 ]
default: 5
max_concurrent_shard_requests:
description:
- Starting with elasticsearch 5.6, you can specify the max concurrent shard per requests.
default: 256
time_field:
description:
- Name of the time field in elasticsearch ds.
- For example C(@timestamp)
default: timestamp
time_interval:
description:
- Minimum group by interval for C(influxdb) or C(elasticsearch) datasources.
- for example C(>10s)
interval:
description:
- For elasticsearch C(ds_type), this is the index pattern used.
choices: ['', 'Hourly', 'Daily', 'Weekly', 'Monthly', 'Yearly']
tsdb_version:
description:
- The opentsdb version.
- Use C(1) for <=2.1, C(2) for ==2.2, C(3) for ==2.3.
choices: [ 1, 2, 3 ]
default: 1
tsdb_resolution:
description:
- The opentsdb time resolution.
choices: [ millisecond, second ]
default: second
sslmode:
description:
- SSL mode for C(postgres) datasource type.
choices: [ disable, require, verify-ca, verify-full ]
trends:
required: false
description:
- Use trends or not for zabbix datasource type
type: bool
version_added: 2.6
client_cert:
required: false
description:
- TLS certificate path used by ansible to query grafana api
version_added: 2.8
client_key:
required: false
description:
- TLS private key path used by ansible to query grafana api
version_added: 2.8
validate_certs:
description:
- Whether to validate the Grafana certificate.
type: bool
default: 'yes'
use_proxy:
description:
- Boolean of whether or not to use proxy.
default: 'yes'
type: bool
version_added: 2.8
aws_auth_type:
description:
- Type for AWS authentication for CloudWatch datasource type (authType of grafana api)
default: 'keys'
choices: [ keys, credentials, arn ]
version_added: 2.8
aws_default_region:
description:
- AWS default region for CloudWatch datasource type
default: 'us-east-1'
choices: [
ap-northeast-1, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-south-1,
ca-central-1,
cn-north-1, cn-northwest-1,
eu-central-1, eu-west-1, eu-west-2, eu-west-3,
sa-east-1,
us-east-1, us-east-2, us-gov-west-1, us-west-1, us-west-2
]
version_added: 2.8
aws_credentials_profile:
description:
- Profile for AWS credentials for CloudWatch datasource type when C(aws_auth_type) is C(credentials)
default: ''
required: false
version_added: 2.8
aws_access_key:
description:
- AWS access key for CloudWatch datasource type when C(aws_auth_type) is C(keys)
default: ''
required: false
version_added: 2.8
aws_secret_key:
description:
- AWS secret key for CloudWatch datasource type when C(aws_auth_type) is C(keys)
default: ''
required: false
version_added: 2.8
aws_assume_role_arn:
description:
- AWS IAM role arn to assume for CloudWatch datasource type when C(aws_auth_type) is C(arn)
default: ''
required: false
version_added: 2.8
aws_custom_metrics_namespaces:
description:
- Namespaces of Custom Metrics for CloudWatch datasource type
default: ''
required: false
version_added: 2.8
'''
EXAMPLES = '''
---
- name: Create elasticsearch datasource
grafana_datasource:
name: "datasource-elastic"
grafana_url: "https://grafana.company.com"
grafana_user: "admin"
grafana_password: "xxxxxx"
org_id: "1"
ds_type: "elasticsearch"
ds_url: "https://elastic.company.com:9200"
database: "[logstash_]YYYY.MM.DD"
basic_auth_user: "grafana"
basic_auth_password: "******"
time_field: "@timestamp"
time_interval: "1m"
interval: "Daily"
es_version: 56
max_concurrent_shard_requests: 42
tls_ca_cert: "/etc/ssl/certs/ca.pem"
- name: Create influxdb datasource
grafana_datasource:
name: "datasource-influxdb"
grafana_url: "https://grafana.company.com"
grafana_user: "admin"
grafana_password: "xxxxxx"
org_id: "1"
ds_type: "influxdb"
ds_url: "https://influx.company.com:8086"
database: "telegraf"
time_interval: ">10s"
tls_ca_cert: "/etc/ssl/certs/ca.pem"
- name: Create postgres datasource
grafana_datasource:
name: "datasource-postgres"
grafana_url: "https://grafana.company.com"
grafana_user: "admin"
grafana_password: "xxxxxx"
org_id: "1"
ds_type: "postgres"
ds_url: "postgres.company.com:5432"
database: "db"
user: "postgres"
password: "iampgroot"
sslmode: "verify-full"
- name: Create cloudwatch datasource
grafana_datasource:
name: "datasource-cloudwatch"
grafana_url: "https://grafana.company.com"
grafana_user: "admin"
grafana_password: "xxxxxx"
org_id: "1"
ds_type: "cloudwatch"
url: "http://monitoring.us-west-1.amazonaws.com"
aws_auth_type: "keys"
aws_default_region: "us-west-1"
aws_access_key: "speakFriendAndEnter"
aws_secret_key: "mel10n"
aws_custom_metrics_namespaces: "n1,n2"
'''
RETURN = '''
---
name:
description: name of the datasource created.
returned: success
type: str
sample: test-ds
id:
description: Id of the datasource
returned: success
type: int
sample: 42
before:
description: datasource returned by grafana api
returned: changed
type: dict
sample: { "access": "proxy",
"basicAuth": false,
"database": "test_*",
"id": 1035,
"isDefault": false,
"jsonData": {
"esVersion": 5,
"timeField": "@timestamp",
"timeInterval": "1m",
},
"name": "grafana_datasource_test",
"orgId": 1,
"type": "elasticsearch",
"url": "http://elastic.company.com:9200",
"user": "",
"password": "",
"withCredentials": false }
after:
description: datasource updated by module
returned: changed
type: dict
sample: { "access": "proxy",
"basicAuth": false,
"database": "test_*",
"id": 1035,
"isDefault": false,
"jsonData": {
"esVersion": 5,
"timeField": "@timestamp",
"timeInterval": "10s",
},
"name": "grafana_datasource_test",
"orgId": 1,
"type": "elasticsearch",
"url": "http://elastic.company.com:9200",
"user": "",
"password": "",
"withCredentials": false }
'''
import json
import base64
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves.urllib.parse import quote
from ansible.module_utils.urls import fetch_url, url_argument_spec
from ansible.module_utils._text import to_text
__metaclass__ = type
class GrafanaAPIException(Exception):
pass
def grafana_switch_organisation(module, grafana_url, org_id, headers):
r, info = fetch_url(module, '%s/api/user/using/%d' % (grafana_url, org_id), headers=headers, method='POST')
if info['status'] != 200:
raise GrafanaAPIException('Unable to switch to organization %s : %s' % (org_id, info))
def grafana_headers(module, data):
headers = {'content-type': 'application/json; charset=utf8'}
if 'grafana_api_key' in data and data['grafana_api_key']:
headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
else:
module.params['force_basic_auth'] = True
grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)
return headers
def grafana_datasource_exists(module, grafana_url, name, headers):
datasource_exists = False
ds = {}
r, info = fetch_url(module, '%s/api/datasources/name/%s' % (grafana_url, quote(name)), headers=headers, method='GET')
if info['status'] == 200:
datasource_exists = True
ds = json.loads(to_text(r.read(), errors='surrogate_or_strict'))
elif info['status'] == 404:
datasource_exists = False
else:
raise GrafanaAPIException('Unable to get datasource %s : %s' % (name, info))
return datasource_exists, ds
def grafana_create_datasource(module, data):
# define data payload for grafana API
payload = {'orgId': data['org_id'],
'name': data['name'],
'type': data['ds_type'],
'access': data['access'],
'url': data['url'],
'database': data['database'],
'withCredentials': data['with_credentials'],
'isDefault': data['is_default'],
'user': data['user'],
'password': data['password']}
# define basic auth
if 'basic_auth_user' in data and data['basic_auth_user'] and 'basic_auth_password' in data and data['basic_auth_password']:
payload['basicAuth'] = True
payload['basicAuthUser'] = data['basic_auth_user']
payload['basicAuthPassword'] = data['basic_auth_password']
else:
payload['basicAuth'] = False
# define tls auth
json_data = {}
if data.get('tls_client_cert') and data.get('tls_client_key'):
json_data['tlsAuth'] = True
if data.get('tls_ca_cert'):
payload['secureJsonData'] = {
'tlsCACert': data['tls_ca_cert'],
'tlsClientCert': data['tls_client_cert'],
'tlsClientKey': data['tls_client_key']
}
json_data['tlsAuthWithCACert'] = True
else:
payload['secureJsonData'] = {
'tlsClientCert': data['tls_client_cert'],
'tlsClientKey': data['tls_client_key']
}
else:
json_data['tlsAuth'] = False
json_data['tlsAuthWithCACert'] = False
if data.get('tls_ca_cert'):
payload['secureJsonData'] = {
'tlsCACert': data['tls_ca_cert']
}
if data.get('tls_skip_verify'):
json_data['tlsSkipVerify'] = True
# datasource type related parameters
if data['ds_type'] == 'elasticsearch':
json_data['esVersion'] = data['es_version']
json_data['timeField'] = data['time_field']
if data.get('interval'):
json_data['interval'] = data['interval']
if data['es_version'] >= 56:
json_data['maxConcurrentShardRequests'] = data['max_concurrent_shard_requests']
if data['ds_type'] == 'elasticsearch' or data['ds_type'] == 'influxdb':
if data.get('time_interval'):
json_data['timeInterval'] = data['time_interval']
if data['ds_type'] == 'opentsdb':
json_data['tsdbVersion'] = data['tsdb_version']
if data['tsdb_resolution'] == 'second':
json_data['tsdbResolution'] = 1
else:
json_data['tsdbResolution'] = 2
if data['ds_type'] == 'postgres':
json_data['sslmode'] = data['sslmode']
if data.get('password'):
payload['secureJsonData'] = {'password': data.get('password')}
if data['ds_type'] == 'alexanderzobnin-zabbix-datasource':
if data.get('trends'):
json_data['trends'] = True
if data['ds_type'] == 'cloudwatch':
if data.get('aws_credentials_profle'):
payload['database'] = data.get('aws_credentials_profile')
json_data['authType'] = data['aws_auth_type']
json_data['defaultRegion'] = data['aws_default_region']
if data.get('aws_custom_metrics_namespaces'):
json_data['customMetricsNamespaces'] = data.get('aws_custom_metrics_namespaces')
if data.get('aws_assume_role_arn'):
json_data['assumeRoleArn'] = data.get('aws_assume_role_arn')
if data.get('aws_access_key') and data.get('aws_secret_key'):
payload['secureJsonData'] = {'accessKey': data.get('aws_access_key'), 'secretKey': data.get('aws_secret_key')}
payload['jsonData'] = json_data
# define http header
headers = grafana_headers(module, data)
# test if datasource already exists
datasource_exists, ds = grafana_datasource_exists(module, data['grafana_url'], data['name'], headers=headers)
result = {}
if datasource_exists is True:
del ds['typeLogoUrl']
if ds.get('version'):
del ds['version']
if ds.get('readOnly'):
del ds['readOnly']
if ds['basicAuth'] is False:
del ds['basicAuthUser']
del ds['basicAuthPassword']
if 'jsonData' in ds:
if 'tlsAuth' in ds['jsonData'] and ds['jsonData']['tlsAuth'] is False:
del ds['secureJsonFields']
if 'tlsAuth' not in ds['jsonData']:
del ds['secureJsonFields']
payload['id'] = ds['id']
if ds == payload:
# unchanged
result['name'] = data['name']
result['id'] = ds['id']
result['msg'] = "Datasource %s unchanged." % data['name']
result['changed'] = False
else:
# update
r, info = fetch_url(module, '%s/api/datasources/%d' % (data['grafana_url'], ds['id']), data=json.dumps(payload), headers=headers, method='PUT')
if info['status'] == 200:
res = json.loads(to_text(r.read(), errors='surrogate_or_strict'))
result['name'] = data['name']
result['id'] = ds['id']
result['before'] = ds
result['after'] = payload
result['msg'] = "Datasource %s updated %s" % (data['name'], res['message'])
result['changed'] = True
else:
raise GrafanaAPIException('Unable to update the datasource id %d : %s' % (ds['id'], info))
else:
# create
r, info = fetch_url(module, '%s/api/datasources' % data['grafana_url'], data=json.dumps(payload), headers=headers, method='POST')
if info['status'] == 200:
res = json.loads(to_text(r.read(), errors='surrogate_or_strict'))
result['msg'] = "Datasource %s created : %s" % (data['name'], res['message'])
result['changed'] = True
result['name'] = data['name']
result['id'] = res['id']
else:
raise GrafanaAPIException('Unable to create the new datasource %s : %s - %s.' % (data['name'], info['status'], info))
return result
def grafana_delete_datasource(module, data):
headers = grafana_headers(module, data)
# test if datasource already exists
datasource_exists, ds = grafana_datasource_exists(module, data['grafana_url'], data['name'], headers=headers)
result = {}
if datasource_exists is True:
# delete
r, info = fetch_url(module, '%s/api/datasources/name/%s' % (data['grafana_url'], quote(data['name'])), headers=headers, method='DELETE')
if info['status'] == 200:
res = json.loads(to_text(r.read(), errors='surrogate_or_strict'))
result['msg'] = "Datasource %s deleted : %s" % (data['name'], res['message'])
result['changed'] = True
result['name'] = data['name']
result['id'] = 0
else:
raise GrafanaAPIException('Unable to update the datasource id %s : %s' % (ds['id'], info))
else:
# datasource does not exist, do nothing
result = {'msg': "Datasource %s does not exist." % data['name'],
'changed': False,
'id': 0,
'name': data['name']}
return result
def main():
# use the predefined argument spec for url
argument_spec = url_argument_spec()
# remove unnecessary arguments
del argument_spec['force']
del argument_spec['force_basic_auth']
del argument_spec['http_agent']
argument_spec.update(
name=dict(required=True, type='str'),
state=dict(choices=['present', 'absent'],
default='present'),
grafana_url=dict(type='str', required=True),
url_username=dict(aliases=['grafana_user'], default='admin'),
url_password=dict(aliases=['grafana_password'], default='admin', no_log=True),
ds_type=dict(choices=['graphite',
'prometheus',
'elasticsearch',
'influxdb',
'opentsdb',
'mysql',
'postgres',
'cloudwatch',
'alexanderzobnin-zabbix-datasource']),
url=dict(required=True, type='str', aliases=['ds_url']),
access=dict(default='proxy', choices=['proxy', 'direct']),
grafana_api_key=dict(type='str', no_log=True),
database=dict(type='str'),
user=dict(default='', type='str'),
password=dict(default='', no_log=True, type='str'),
basic_auth_user=dict(type='str'),
basic_auth_password=dict(type='str', no_log=True),
with_credentials=dict(default=False, type='bool'),
tls_client_cert=dict(type='str', no_log=True),
tls_client_key=dict(type='str', no_log=True),
tls_ca_cert=dict(type='str', no_log=True),
tls_skip_verify=dict(type='bool', default=False),
is_default=dict(default=False, type='bool'),
org_id=dict(default=1, type='int'),
es_version=dict(type='int', default=5, choices=[2, 5, 56]),
max_concurrent_shard_requests=dict(type='int', default=256),
time_field=dict(default='@timestamp', type='str'),
time_interval=dict(type='str'),
interval=dict(type='str', choices=['', 'Hourly', 'Daily', 'Weekly', 'Monthly', 'Yearly'], default=''),
tsdb_version=dict(type='int', default=1, choices=[1, 2, 3]),
tsdb_resolution=dict(type='str', default='second', choices=['second', 'millisecond']),
sslmode=dict(default='disable', choices=['disable', 'require', 'verify-ca', 'verify-full']),
trends=dict(default=False, type='bool'),
aws_auth_type=dict(default='keys', choices=['keys', 'credentials', 'arn']),
aws_default_region=dict(default='us-east-1', choices=['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1',
'ca-central-1',
'cn-north-1', 'cn-northwest-1',
'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3',
'sa-east-1',
'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2']),
aws_access_key=dict(default='', no_log=True, type='str'),
aws_secret_key=dict(default='', no_log=True, type='str'),
aws_credentials_profile=dict(default='', type='str'),
aws_assume_role_arn=dict(default='', type='str'),
aws_custom_metrics_namespaces=dict(type='str'),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=[['url_username', 'url_password', 'org_id'], ['tls_client_cert', 'tls_client_key']],
mutually_exclusive=[['url_username', 'grafana_api_key'], ['tls_ca_cert', 'tls_skip_verify']],
required_if=[
['ds_type', 'opentsdb', ['tsdb_version', 'tsdb_resolution']],
['ds_type', 'influxdb', ['database']],
['ds_type', 'elasticsearch', ['database', 'es_version', 'time_field', 'interval']],
['ds_type', 'mysql', ['database']],
['ds_type', 'postgres', ['database', 'sslmode']],
['ds_type', 'cloudwatch', ['aws_auth_type', 'aws_default_region']],
['es_version', 56, ['max_concurrent_shard_requests']]
],
)
try:
if module.params['state'] == 'present':
result = grafana_create_datasource(module, module.params)
else:
result = grafana_delete_datasource(module, module.params)
except GrafanaAPIException as e:
module.fail_json(
failed=True,
msg="error %s : %s " % (type(e), e)
)
return
module.exit_json(
failed=False,
**result
)
return
if __name__ == '__main__':
main()

@ -1,257 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Thierry Sallé (@seuf)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
ANSIBLE_METADATA = {
'status': ['preview'],
'supported_by': 'community',
'metadata_version': '1.1'
}
DOCUMENTATION = '''
---
module: grafana_plugin
author:
- Thierry Sallé (@seuf)
version_added: "2.5"
short_description: Manage Grafana plugins via grafana-cli
description:
- Install and remove Grafana plugins.
- See U(https://grafana.com/docs/plugins/installation/) for upstream documentation.
options:
name:
description:
- Name of the plugin.
required: true
version:
description:
- Version of the plugin to install.
- Defaults to C(latest).
grafana_plugins_dir:
description:
- Directory where the Grafana plugin will be installed.
- If omitted, defaults to C(/var/lib/grafana/plugins).
grafana_repo:
description:
- URL to the Grafana plugin repository.
- "If omitted, grafana-cli will use the default value: U(https://grafana.com/api/plugins)."
grafana_plugin_url:
description:
- Full URL to the plugin zip file instead of downloading the file from U(https://grafana.com/api/plugins).
- Requires grafana 4.6.x or later.
state:
description:
- Whether the plugin should be installed.
choices:
- present
- absent
default: present
'''
EXAMPLES = '''
---
- name: Install/update Grafana piechart panel plugin
grafana_plugin:
name: grafana-piechart-panel
version: latest
state: present
'''
RETURN = '''
---
version:
description: version of the installed/removed/updated plugin.
type: str
returned: always
'''
import base64
import json
import os
from ansible.module_utils.basic import AnsibleModule
__metaclass__ = type
class GrafanaCliException(Exception):
pass
def grafana_cli_bin(params):
'''
Get the grafana-cli binary path with global options.
Raise a GrafanaCliException if the grafana-cli is not present or not in PATH
:param params: ansible module params. Used to fill grafana-cli global params.
'''
program = 'grafana-cli'
grafana_cli = None
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
grafana_cli = program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
grafana_cli = exe_file
break
if grafana_cli is None:
raise GrafanaCliException('grafana-cli binary is not present or not in PATH')
else:
if 'grafana_plugin_url' in params and params['grafana_plugin_url']:
grafana_cli = '{0} {1} {2}'.format(grafana_cli, '--pluginUrl', params['grafana_plugin_url'])
if 'grafana_plugins_dir' in params and params['grafana_plugins_dir']:
grafana_cli = '{0} {1} {2}'.format(grafana_cli, '--pluginsDir', params['grafana_plugins_dir'])
if 'grafana_repo' in params and params['grafana_repo']:
grafana_cli = '{0} {1} {2}'.format(grafana_cli, '--repo', params['grafana_repo'])
if 'validate_certs' in params and params['validate_certs'] is False:
grafana_cli = '{0} {1}'.format(grafana_cli, '--insecure')
return '{0} {1}'.format(grafana_cli, 'plugins')
def get_grafana_plugin_version(module, params):
'''
Fetch grafana installed plugin version. Return None if plugin is not installed.
:param module: ansible module object. used to run system commands.
:param params: ansible module params.
'''
grafana_cli = grafana_cli_bin(params)
rc, stdout, stderr = module.run_command('{0} ls'.format(grafana_cli))
stdout_lines = stdout.split("\n")
for line in stdout_lines:
if line.find(' @ ') != -1:
line = line.rstrip()
plugin_name, plugin_version = line.split(' @ ')
if plugin_name == params['name']:
return plugin_version
return None
def get_grafana_plugin_version_latest(module, params):
'''
Fetch the latest version available from grafana-cli.
Return the newest version number or None not found.
:param module: ansible module object. used to run system commands.
:param params: ansible module params.
'''
grafana_cli = grafana_cli_bin(params)
rc, stdout, stderr = module.run_command('{0} list-versions {1}'.format(grafana_cli,
params['name']))
stdout_lines = stdout.split("\n")
if stdout_lines[0]:
return stdout_lines[0].rstrip()
return None
def grafana_plugin(module, params):
'''
Install update or remove grafana plugin
:param module: ansible module object. used to run system commands.
:param params: ansible module params.
'''
grafana_cli = grafana_cli_bin(params)
if params['state'] == 'present':
grafana_plugin_version = get_grafana_plugin_version(module, params)
if grafana_plugin_version is not None:
if 'version' in params and params['version']:
if params['version'] == grafana_plugin_version:
return {'msg': 'Grafana plugin already installed',
'changed': False,
'version': grafana_plugin_version}
else:
if params['version'] == 'latest' or params['version'] is None:
latest_version = get_grafana_plugin_version_latest(module, params)
if latest_version == grafana_plugin_version:
return {'msg': 'Grafana plugin already installed',
'changed': False,
'version': grafana_plugin_version}
cmd = '{0} update {1}'.format(grafana_cli, params['name'])
else:
cmd = '{0} install {1} {2}'.format(grafana_cli, params['name'], params['version'])
else:
return {'msg': 'Grafana plugin already installed',
'changed': False,
'version': grafana_plugin_version}
else:
if 'version' in params:
if params['version'] == 'latest' or params['version'] is None:
cmd = '{0} install {1}'.format(grafana_cli, params['name'])
else:
cmd = '{0} install {1} {2}'.format(grafana_cli, params['name'], params['version'])
else:
cmd = '{0} install {1}'.format(grafana_cli, params['name'])
else:
cmd = '{0} uninstall {1}'.format(grafana_cli, params['name'])
rc, stdout, stderr = module.run_command(cmd)
if rc == 0:
stdout_lines = stdout.split("\n")
for line in stdout_lines:
if line.find(params['name']):
if line.find(' @ ') != -1:
line = line.rstrip()
plugin_name, plugin_version = line.split(' @ ')
else:
plugin_version = None
return {'msg': 'Grafana plugin {0} installed : {1}'.format(params['name'], cmd),
'changed': True,
'version': plugin_version}
else:
raise GrafanaCliException("'{0}' execution returned an error : [{1}] {2} {3}".format(cmd, rc, stdout, stderr))
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(required=True,
type='str'),
version=dict(type='str'),
grafana_plugins_dir=dict(type='str'),
grafana_repo=dict(type='str'),
grafana_plugin_url=dict(type='str'),
state=dict(choices=['present', 'absent'],
default='present')
),
supports_check_mode=False
)
try:
result = grafana_plugin(module, module.params)
except GrafanaCliException as e:
module.fail_json(
failed=True,
msg="{0}".format(e)
)
return
except Exception as e:
module.fail_json(
failed=True,
msg="{0} : {1} ".format(type(e), e)
)
return
module.exit_json(
failed=False,
**result
)
return
if __name__ == '__main__':
main()

@ -1,273 +0,0 @@
# -*- 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 <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import socket
import getpass
from base64 import b64encode
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
DOCUMENTATION = """
callback: grafana_annotations
callback_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)
version_added: "2.6"
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
"""
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 = <path_to_callback_plugins_folder>
callback_whitelist = grafana_annotations
and put the plugin in <path_to_callback_plugins_folder>
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = '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._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._send_annotations(data)
def v2_runner_on_failed(self, result, **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))
data = {
'time': to_millis(datetime.now()),
'text': text,
'tags': ['ansible', 'ansible_event_failure', self.playbook]
}
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:
response = 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))

@ -1,174 +0,0 @@
# (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = """
lookup: grafana_dashboard
author: Thierry Salle (@seuf)
version_added: "2.7"
short_description: list or search grafana dashboards
description:
- This lookup returns a list of grafana dashboards with possibility to filter them by query.
options:
grafana_url:
description: url of grafana.
env:
- name: GRAFANA_URL
default: http://127.0.0.1:3000
grafana_api_key:
description:
- api key of grafana.
- when C(grafana_api_key) is set, the options C(grafan_user), C(grafana_password) and C(grafana_org_id) are ignored.
- Attention, please remove the two == at the end of the grafana_api_key
- because ansible lookup plugins options are split on = (see example).
env:
- name: GRAFANA_API_KEY
grafana_user:
description: grafana authentication user.
env:
- name: GRAFANA_USER
default: admin
grafana_password:
description: grafana authentication password.
env:
- name: GRAFANA_PASSWORD
default: admin
grafana_org_id:
description: grafana organisation id.
env:
- name: GRAFANA_ORG_ID
default: 1
search:
description: optional filter for dashboard search.
env:
- name: GRAFANA_DASHBOARD_SEARCH
"""
EXAMPLES = """
- name: get project foo grafana dashboards
set_fact:
grafana_dashboards: "{{ lookup('grafana_dashboard', 'grafana_url=http://grafana.company.com grafana_user=admin grafana_password=admin search=foo') }}"
- name: get all grafana dashboards
set_fact:
grafana_dashboards: "{{ lookup('grafana_dashboard', 'grafana_url=http://grafana.company.com grafana_api_key=' ~ grafana_api_key|replace('==', '')) }}"
"""
import base64
import json
import os
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.plugins.lookup import LookupBase
from ansible.module_utils.urls import basic_auth_header, open_url
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.utils.display import Display
display = Display()
ANSIBLE_GRAFANA_URL = 'http://127.0.0.1:3000'
ANSIBLE_GRAFANA_API_KEY = None
ANSIBLE_GRAFANA_USER = 'admin'
ANSIBLE_GRAFANA_PASSWORD = 'admin'
ANSIBLE_GRAFANA_ORG_ID = 1
ANSIBLE_GRAFANA_DASHBOARD_SEARCH = None
if os.getenv('GRAFANA_URL') is not None:
ANSIBLE_GRAFANA_URL = os.environ['GRAFANA_URL']
if os.getenv('GRAFANA_API_KEY') is not None:
ANSIBLE_GRAFANA_API_KEY = os.environ['GRAFANA_API_KEY']
if os.getenv('GRAFANA_USER') is not None:
ANSIBLE_GRAFANA_USER = os.environ['GRAFANA_USER']
if os.getenv('GRAFANA_PASSWORD') is not None:
ANSIBLE_GRAFANA_PASSWORD = os.environ['GRAFANA_PASSWORD']
if os.getenv('GRAFANA_ORG_ID') is not None:
ANSIBLE_GRAFANA_ORG_ID = os.environ['GRAFANA_ORG_ID']
if os.getenv('GRAFANA_DASHBOARD_SEARCH') is not None:
ANSIBLE_GRAFANA_DASHBOARD_SEARCH = os.environ['GRAFANA_DASHBOARD_SEARCH']
class GrafanaAPIException(Exception):
pass
class GrafanaAPI:
def __init__(self, **kwargs):
self.grafana_url = kwargs.get('grafana_url', ANSIBLE_GRAFANA_URL)
self.grafana_api_key = kwargs.get('grafana_api_key', ANSIBLE_GRAFANA_API_KEY)
self.grafana_user = kwargs.get('grafana_user', ANSIBLE_GRAFANA_USER)
self.grafana_password = kwargs.get('grafana_password', ANSIBLE_GRAFANA_PASSWORD)
self.grafana_org_id = kwargs.get('grafana_org_id', ANSIBLE_GRAFANA_ORG_ID)
self.search = kwargs.get('search', ANSIBLE_GRAFANA_DASHBOARD_SEARCH)
def grafana_switch_organisation(self, headers):
try:
r = open_url('%s/api/user/using/%s' % (self.grafana_url, self.grafana_org_id), headers=headers, method='POST')
except HTTPError as e:
raise GrafanaAPIException('Unable to switch to organization %s : %s' % (self.grafana_org_id, to_native(e)))
if r.getcode() != 200:
raise GrafanaAPIException('Unable to switch to organization %s : %s' % (self.grafana_org_id, str(r.getcode())))
def grafana_headers(self):
headers = {'content-type': 'application/json; charset=utf8'}
if self.grafana_api_key:
headers['Authorization'] = "Bearer %s==" % self.grafana_api_key
else:
headers['Authorization'] = basic_auth_header(self.grafana_user, self.grafana_password)
self.grafana_switch_organisation(headers)
return headers
def grafana_list_dashboards(self):
# define http headers
headers = self.grafana_headers()
dashboard_list = []
try:
if self.search:
r = open_url('%s/api/search?query=%s' % (self.grafana_url, self.search), headers=headers, method='GET')
else:
r = open_url('%s/api/search/' % self.grafana_url, headers=headers, method='GET')
except HTTPError as e:
raise GrafanaAPIException('Unable to search dashboards : %s' % to_native(e))
if r.getcode() == 200:
try:
dashboard_list = json.loads(r.read())
except Exception as e:
raise GrafanaAPIException('Unable to parse json list %s' % to_native(e))
else:
raise GrafanaAPIException('Unable to list grafana dashboards : %s' % str(r.getcode()))
return dashboard_list
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
grafana_args = terms[0].split(' ')
grafana_dict = {}
ret = []
for param in grafana_args:
try:
key, value = param.split('=')
except ValueError:
raise AnsibleError("grafana_dashboard lookup plugin needs key=value pairs, but received %s" % terms)
grafana_dict[key] = value
grafana = GrafanaAPI(**grafana_dict)
ret = grafana.grafana_list_dashboards()
return ret

@ -1,2 +0,0 @@
shippable/posix/group1
skip/aix

@ -1,67 +0,0 @@
---
- name: Create cloudwatch datasource
grafana_datasource:
name: "datasource-cloudwatch"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "cloudwatch"
url: "http://monitoring.us-west-1.amazonaws.com"
aws_auth_type: "keys"
aws_default_region: "us-west-1"
aws_access_key: "speakFriendAndEnter"
aws_secret_key: "mel10n"
aws_custom_metrics_namespaces: "n1,n2"
register: result
- debug:
var: result
- assert:
that:
- "result.changed == true"
- "result.name == 'datasource-cloudwatch'"
- "result.msg == 'Datasource datasource-cloudwatch created : Datasource added'"
- name: Check cloudwatch datasource creation idempotency
grafana_datasource:
name: "datasource-cloudwatch"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "cloudwatch"
url: "http://monitoring.us-west-1.amazonaws.com"
aws_auth_type: "keys"
aws_default_region: "us-west-1"
aws_access_key: "speakFriendAndEnter"
aws_secret_key: "mel10n"
aws_custom_metrics_namespaces: "n1,n2"
register: result
- debug:
var: result
- assert:
that:
# Idempotency is not working currently
# "result.changed == false"
- "result.name == 'datasource-cloudwatch'"
- "result.after.access == 'proxy'"
- "result.after.basicAuth == false"
- "result.after.database == None"
- "result.after.isDefault == false"
- "result.after.jsonData.authType == 'keys'"
- "result.after.jsonData.customMetricsNamespaces == 'n1,n2'"
- "result.after.jsonData.defaultRegion == 'us-west-1'"
- "result.after.jsonData.tlsAuth == false"
- "result.after.jsonData.tlsAuthWithCACert == false"
- "result.after.name == 'datasource-cloudwatch'"
- "result.after.orgId == 1"
- "result.after.password == ''"
- "result.after.type == 'cloudwatch'"
- "result.after.url == 'http://monitoring.us-west-1.amazonaws.com'"
- "result.after.user == ''"
- "result.after.withCredentials == false"

@ -1,77 +0,0 @@
---
- name: Create elasticsearch datasource
grafana_datasource:
name: "datasource-elastic"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "elasticsearch"
ds_url: "https://elastic.company.com:9200"
database: "[logstash_]YYYY.MM.DD"
basic_auth_user: "grafana"
basic_auth_password: "******"
time_field: "@timestamp"
time_interval: "1m"
interval: "Daily"
es_version: 56
max_concurrent_shard_requests: 42
tls_ca_cert: "/etc/ssl/certs/ca.pem"
register: result
- debug:
var: result
- assert:
that:
- "result.changed == true"
- "result.name == 'datasource-elastic'"
- "result.msg == 'Datasource datasource-elastic created : Datasource added'"
- name: Check elasticsearch datasource creation idempotency
grafana_datasource:
name: "datasource-elastic"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "elasticsearch"
ds_url: "https://elastic.company.com:9200"
database: "[logstash_]YYYY.MM.DD"
basic_auth_user: "grafana"
basic_auth_password: "******"
time_field: "@timestamp"
time_interval: "1m"
interval: "Daily"
es_version: 56
max_concurrent_shard_requests: 42
tls_ca_cert: "/etc/ssl/certs/ca.pem"
register: result
- debug:
var: result
- assert:
that:
# Idempotency is not working currently
# "result.changed == false"
- "result.name == 'datasource-elastic'"
- "result.after.basicAuth == true"
- "result.after.basicAuthUser == 'grafana'"
- "result.after.access == 'proxy'"
- "result.after.database == '[logstash_]YYYY.MM.DD'"
- "result.after.isDefault == false"
- "result.after.jsonData.esVersion == 56"
- "result.after.jsonData.interval == 'Daily'"
- "result.after.jsonData.maxConcurrentShardRequests == 42"
- "result.after.jsonData.timeField == '@timestamp'"
- "result.after.jsonData.tlsAuth == false"
- "result.after.jsonData.tlsAuthWithCACert == false"
- "result.after.name == 'datasource-elastic'"
- "result.after.orgId == 1"
- "result.after.password == ''"
- "result.after.type == 'elasticsearch'"
- "result.after.url == 'https://elastic.company.com:9200'"
- "result.after.user == ''"
- "result.after.withCredentials == false"

@ -1,61 +0,0 @@
---
- name: Create influxdb datasource
grafana_datasource:
name: "datasource-influxdb"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "influxdb"
ds_url: "https://influx.company.com:8086"
database: "telegraf"
time_interval: ">10s"
tls_ca_cert: "/etc/ssl/certs/ca.pem"
register: result
- debug:
var: result
- assert:
that:
- "result.changed == true"
- "result.name == 'datasource-influxdb'"
- "result.msg == 'Datasource datasource-influxdb created : Datasource added'"
- name: Check influxdb datasource creation idempotency
grafana_datasource:
name: "datasource-influxdb"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "influxdb"
ds_url: "https://influx.company.com:8086"
database: "telegraf"
time_interval: ">10s"
tls_ca_cert: "/etc/ssl/certs/ca.pem"
register: result
- debug:
var: result
- assert:
that:
# Idempotency is not working currently
# "result.changed == false"
- "result.name == 'datasource-influxdb'"
- "result.after.basicAuth == false"
- "result.after.access == 'proxy'"
- "result.after.database == 'telegraf'"
- "result.after.isDefault == false"
- "result.after.jsonData.timeInterval == '>10s'"
- "result.after.jsonData.tlsAuth == false"
- "result.after.jsonData.tlsAuthWithCACert == false"
- "result.after.name == 'datasource-influxdb'"
- "result.after.orgId == 1"
- "result.after.password == ''"
- "result.after.type == 'influxdb'"
- "result.after.url == 'https://influx.company.com:8086'"
- "result.after.user == ''"
- "result.after.withCredentials == false"

@ -1,8 +0,0 @@
- block:
- include: elastic.yml
- include: influx.yml
- include: postgres.yml
- include: cloudwatch.yml
when:
- ansible_distribution == 'Ubuntu'
- ansible_distribution_release != 'trusty'

@ -1,61 +0,0 @@
---
- name: Create postgres datasource
grafana_datasource:
name: "datasource-postgres"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "postgres"
ds_url: "postgres.company.com:5432"
database: "db"
user: "postgres"
password: "iampgroot"
sslmode: "verify-full"
register: result
- debug:
var: result
- assert:
that:
- "result.changed == true"
- "result.name == 'datasource-postgres'"
- "result.msg == 'Datasource datasource-postgres created : Datasource added'"
- name: Check postgres datasource creation idempotency
grafana_datasource:
name: "datasource-postgres"
grafana_url: "http://127.0.0.1:3000"
grafana_user: "admin"
grafana_password: "admin"
org_id: "1"
ds_type: "postgres"
ds_url: "postgres.company.com:5432"
database: "db"
user: "postgres"
password: "iampgroot"
sslmode: "verify-full"
register: result
- debug:
var: result
- assert:
that:
# Idempotency is not working currently
# "result.changed == false"
- "result.name == 'datasource-postgres'"
- "result.after.basicAuth == false"
- "result.after.database == 'db'"
- "result.after.isDefault == false"
- "result.after.jsonData.sslmode == 'verify-full'"
- "result.after.jsonData.tlsAuth == false"
- "result.after.jsonData.tlsAuthWithCACert == false"
- "result.after.name == 'datasource-postgres'"
- "result.after.orgId == 1"
- "result.after.type == 'postgres'"
- "result.after.url == 'postgres.company.com:5432'"
- "result.after.user == 'postgres'"
- "result.after.withCredentials == false"

@ -1740,17 +1740,6 @@ lib/ansible/modules/files/synchronize.py validate-modules:undocumented-parameter
lib/ansible/modules/files/unarchive.py validate-modules:nonexistent-parameter-documented
lib/ansible/modules/files/unarchive.py validate-modules:parameter-list-no-elements
lib/ansible/modules/identity/cyberark/cyberark_authentication.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/monitoring/grafana/grafana_dashboard.py validate-modules:doc-missing-type
lib/ansible/modules/monitoring/grafana/grafana_dashboard.py validate-modules:doc-required-mismatch
lib/ansible/modules/monitoring/grafana/grafana_dashboard.py validate-modules:invalid-argument-name
lib/ansible/modules/monitoring/grafana/grafana_dashboard.py validate-modules:mutually_exclusive-unknown
lib/ansible/modules/monitoring/grafana/grafana_dashboard.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/monitoring/grafana/grafana_datasource.py validate-modules:doc-default-does-not-match-spec
lib/ansible/modules/monitoring/grafana/grafana_datasource.py validate-modules:doc-missing-type
lib/ansible/modules/monitoring/grafana/grafana_datasource.py validate-modules:doc-required-mismatch
lib/ansible/modules/monitoring/grafana/grafana_datasource.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/monitoring/grafana/grafana_plugin.py validate-modules:doc-missing-type
lib/ansible/modules/monitoring/grafana/grafana_plugin.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/net_tools/basics/get_url.py validate-modules:parameter-type-not-in-doc
lib/ansible/modules/net_tools/basics/uri.py pylint:blacklisted-name
lib/ansible/modules/net_tools/basics/uri.py validate-modules:doc-required-mismatch

Loading…
Cancel
Save