From 43f47d553ed1fdf7aa482eb7e8623fe9e3c2d993 Mon Sep 17 00:00:00 2001 From: Ansible Core Team Date: Mon, 9 Mar 2020 09:40:30 +0000 Subject: [PATCH] Migrated to community.grafana --- .../monitoring/grafana/grafana_dashboard.py | 556 -------------- .../monitoring/grafana/grafana_datasource.py | 683 ------------------ .../monitoring/grafana/grafana_plugin.py | 257 ------- .../plugins/callback/grafana_annotations.py | 273 ------- .../plugins/lookup/grafana_dashboard.py | 174 ----- .../targets/grafana_datasource/aliases | 2 - .../targets/grafana_datasource/meta/main.yml | 2 - .../grafana_datasource/tasks/cloudwatch.yml | 67 -- .../grafana_datasource/tasks/elastic.yml | 77 -- .../grafana_datasource/tasks/influx.yml | 61 -- .../targets/grafana_datasource/tasks/main.yml | 8 - .../grafana_datasource/tasks/postgres.yml | 61 -- test/sanity/ignore.txt | 11 - 13 files changed, 2232 deletions(-) delete mode 100644 lib/ansible/modules/monitoring/grafana/grafana_dashboard.py delete mode 100644 lib/ansible/modules/monitoring/grafana/grafana_datasource.py delete mode 100644 lib/ansible/modules/monitoring/grafana/grafana_plugin.py delete mode 100644 lib/ansible/plugins/callback/grafana_annotations.py delete mode 100644 lib/ansible/plugins/lookup/grafana_dashboard.py delete mode 100644 test/integration/targets/grafana_datasource/aliases delete mode 100644 test/integration/targets/grafana_datasource/meta/main.yml delete mode 100644 test/integration/targets/grafana_datasource/tasks/cloudwatch.yml delete mode 100644 test/integration/targets/grafana_datasource/tasks/elastic.yml delete mode 100644 test/integration/targets/grafana_datasource/tasks/influx.yml delete mode 100644 test/integration/targets/grafana_datasource/tasks/main.yml delete mode 100644 test/integration/targets/grafana_datasource/tasks/postgres.yml diff --git a/lib/ansible/modules/monitoring/grafana/grafana_dashboard.py b/lib/ansible/modules/monitoring/grafana/grafana_dashboard.py deleted file mode 100644 index c0177eb9acc..00000000000 --- a/lib/ansible/modules/monitoring/grafana/grafana_dashboard.py +++ /dev/null @@ -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() diff --git a/lib/ansible/modules/monitoring/grafana/grafana_datasource.py b/lib/ansible/modules/monitoring/grafana/grafana_datasource.py deleted file mode 100644 index 21bc3f667a4..00000000000 --- a/lib/ansible/modules/monitoring/grafana/grafana_datasource.py +++ /dev/null @@ -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() diff --git a/lib/ansible/modules/monitoring/grafana/grafana_plugin.py b/lib/ansible/modules/monitoring/grafana/grafana_plugin.py deleted file mode 100644 index 418253a108f..00000000000 --- a/lib/ansible/modules/monitoring/grafana/grafana_plugin.py +++ /dev/null @@ -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() diff --git a/lib/ansible/plugins/callback/grafana_annotations.py b/lib/ansible/plugins/callback/grafana_annotations.py deleted file mode 100644 index d1d6ed82e7d..00000000000 --- a/lib/ansible/plugins/callback/grafana_annotations.py +++ /dev/null @@ -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 . - -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 = - callback_whitelist = grafana_annotations - and put the plugin in - """ - - 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)) diff --git a/lib/ansible/plugins/lookup/grafana_dashboard.py b/lib/ansible/plugins/lookup/grafana_dashboard.py deleted file mode 100644 index 886bc8f0a01..00000000000 --- a/lib/ansible/plugins/lookup/grafana_dashboard.py +++ /dev/null @@ -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 diff --git a/test/integration/targets/grafana_datasource/aliases b/test/integration/targets/grafana_datasource/aliases deleted file mode 100644 index f8e28c7e469..00000000000 --- a/test/integration/targets/grafana_datasource/aliases +++ /dev/null @@ -1,2 +0,0 @@ -shippable/posix/group1 -skip/aix diff --git a/test/integration/targets/grafana_datasource/meta/main.yml b/test/integration/targets/grafana_datasource/meta/main.yml deleted file mode 100644 index af628245fb3..00000000000 --- a/test/integration/targets/grafana_datasource/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - - setup_grafana diff --git a/test/integration/targets/grafana_datasource/tasks/cloudwatch.yml b/test/integration/targets/grafana_datasource/tasks/cloudwatch.yml deleted file mode 100644 index 1119eb1ac1c..00000000000 --- a/test/integration/targets/grafana_datasource/tasks/cloudwatch.yml +++ /dev/null @@ -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" diff --git a/test/integration/targets/grafana_datasource/tasks/elastic.yml b/test/integration/targets/grafana_datasource/tasks/elastic.yml deleted file mode 100644 index 471a2ece5bb..00000000000 --- a/test/integration/targets/grafana_datasource/tasks/elastic.yml +++ /dev/null @@ -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" diff --git a/test/integration/targets/grafana_datasource/tasks/influx.yml b/test/integration/targets/grafana_datasource/tasks/influx.yml deleted file mode 100644 index 821ea07e186..00000000000 --- a/test/integration/targets/grafana_datasource/tasks/influx.yml +++ /dev/null @@ -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" diff --git a/test/integration/targets/grafana_datasource/tasks/main.yml b/test/integration/targets/grafana_datasource/tasks/main.yml deleted file mode 100644 index 73a1f60f059..00000000000 --- a/test/integration/targets/grafana_datasource/tasks/main.yml +++ /dev/null @@ -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' diff --git a/test/integration/targets/grafana_datasource/tasks/postgres.yml b/test/integration/targets/grafana_datasource/tasks/postgres.yml deleted file mode 100644 index e540515e071..00000000000 --- a/test/integration/targets/grafana_datasource/tasks/postgres.yml +++ /dev/null @@ -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" diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index a912e1f5546..c403c134689 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -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