mirror of https://github.com/ansible/ansible.git
Migrated to community.grafana
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,2 +0,0 @@
|
||||
dependencies:
|
||||
- setup_grafana
|
@ -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"
|
Loading…
Reference in New Issue