From 126c45e646cb273847082d3acae7e3c64d0c8422 Mon Sep 17 00:00:00 2001 From: gardar Date: Wed, 31 May 2023 03:01:26 +0000 Subject: [PATCH] feat: add grafana server role (#48) Signed-off-by: gardar --- .config/molecule/config.yml | 80 ++++++ examples/monitor-multiple-instances.md | 2 +- requirements.yml | 6 + roles/grafana/README.md | 139 ++++++++++ roles/grafana/defaults/main.yml | 262 ++++++++++++++++++ roles/grafana/handlers/main.yml | 28 ++ roles/grafana/meta/main.yml | 31 +++ .../grafana/molecule/alternative/converge.yml | 105 +++++++ .../grafana/molecule/alternative/molecule.yml | 1 + .../alternative/tests/test_alternative.py | 52 ++++ roles/grafana/molecule/default/converge.yml | 10 + roles/grafana/molecule/default/molecule.yml | 1 + .../molecule/default/tests/test_default.py | 50 ++++ roles/grafana/tasks/api_keys.yml | 43 +++ roles/grafana/tasks/configure.yml | 84 ++++++ roles/grafana/tasks/dashboards.yml | 133 +++++++++ roles/grafana/tasks/datasources.yml | 40 +++ roles/grafana/tasks/install.yml | 89 ++++++ roles/grafana/tasks/main.yml | 120 ++++++++ roles/grafana/tasks/notifications.yml | 13 + roles/grafana/tasks/plugins.yml | 20 ++ roles/grafana/tasks/preflight.yml | 84 ++++++ roles/grafana/templates/grafana.ini.j2 | 197 +++++++++++++ roles/grafana/templates/ldap.toml.j2 | 39 +++ roles/grafana/templates/tmpfiles.j2 | 2 + roles/grafana/test-requirements.txt | 6 + roles/grafana/vars/distro/debian.yml | 8 + roles/grafana/vars/distro/redhat.yml | 5 + roles/grafana/vars/distro/suse.yml | 2 + roles/grafana_agent/README.md | 2 +- .../molecule-grafana-alternative/runme.sh | 12 + .../targets/molecule-grafana-default/runme.sh | 12 + 32 files changed, 1676 insertions(+), 2 deletions(-) create mode 100644 .config/molecule/config.yml create mode 100644 requirements.yml create mode 100644 roles/grafana/README.md create mode 100644 roles/grafana/defaults/main.yml create mode 100644 roles/grafana/handlers/main.yml create mode 100644 roles/grafana/meta/main.yml create mode 100644 roles/grafana/molecule/alternative/converge.yml create mode 100644 roles/grafana/molecule/alternative/molecule.yml create mode 100644 roles/grafana/molecule/alternative/tests/test_alternative.py create mode 100644 roles/grafana/molecule/default/converge.yml create mode 100644 roles/grafana/molecule/default/molecule.yml create mode 100644 roles/grafana/molecule/default/tests/test_default.py create mode 100644 roles/grafana/tasks/api_keys.yml create mode 100644 roles/grafana/tasks/configure.yml create mode 100644 roles/grafana/tasks/dashboards.yml create mode 100644 roles/grafana/tasks/datasources.yml create mode 100644 roles/grafana/tasks/install.yml create mode 100644 roles/grafana/tasks/main.yml create mode 100644 roles/grafana/tasks/notifications.yml create mode 100644 roles/grafana/tasks/plugins.yml create mode 100644 roles/grafana/tasks/preflight.yml create mode 100644 roles/grafana/templates/grafana.ini.j2 create mode 100644 roles/grafana/templates/ldap.toml.j2 create mode 100644 roles/grafana/templates/tmpfiles.j2 create mode 100644 roles/grafana/test-requirements.txt create mode 100644 roles/grafana/vars/distro/debian.yml create mode 100644 roles/grafana/vars/distro/redhat.yml create mode 100644 roles/grafana/vars/distro/suse.yml create mode 100755 tests/integration/targets/molecule-grafana-alternative/runme.sh create mode 100755 tests/integration/targets/molecule-grafana-default/runme.sh diff --git a/.config/molecule/config.yml b/.config/molecule/config.yml new file mode 100644 index 0000000..6e627ca --- /dev/null +++ b/.config/molecule/config.yml @@ -0,0 +1,80 @@ +--- +dependency: + name: galaxy +driver: + name: docker +platforms: + - name: almalinux-8 + image: dokken/almalinux-8 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: almalinux-9 + image: dokken/almalinux-9 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: centos-7 + image: dokken/centos-7 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /usr/lib/systemd/systemd + - name: centos-stream-8 + image: dokken/centos-stream-8 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: centos-stream-9 + image: dokken/centos-stream-9 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: debian-10 + image: dokken/debian-10 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: debian-11 + image: dokken/debian-11 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: fedora-36 + image: dokken/fedora-36 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: fedora-37 + image: dokken/fedora-37 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: ubuntu-18.04 + image: dokken/ubuntu-18.04 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: ubuntu-20.04 + image: dokken/ubuntu-20.04 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd + - name: ubuntu-22.04 + image: dokken/ubuntu-22.04 + pre_build_image: true + privileged: true + cgroup_parent: docker.slice + command: /lib/systemd/systemd +verifier: + name: testinfra diff --git a/examples/monitor-multiple-instances.md b/examples/monitor-multiple-instances.md index 875477d..d624c7e 100644 --- a/examples/monitor-multiple-instances.md +++ b/examples/monitor-multiple-instances.md @@ -124,7 +124,7 @@ To use the Grafana Agent Ansible role: ``` The playbook calls the `grafana_agent` role from the `grafana.grafana` Ansible collection. - The Agent configuration in this playbook send metrics and logs from the linux hosts to your prometheus and Loki data sources. + The Agent configuration in this playbook send metrics and logs from the linux hosts to your Prometheus and Loki data sources. Refer to the [Grafana Ansible documentation](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent#role-variables) to understand the other variables you can pass to the `grafana_agent` role. diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..c1917b4 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,6 @@ +--- +collections: + - name: https://github.com/ansible-collections/community.general.git + type: git + - name: https://github.com/ansible-collections/community.grafana.git + type: git diff --git a/roles/grafana/README.md b/roles/grafana/README.md new file mode 100644 index 0000000..a8d9204 --- /dev/null +++ b/roles/grafana/README.md @@ -0,0 +1,139 @@ +

grafana logo

+ +# Ansible Role: grafana.grafana.grafana + +[![License](https://img.shields.io/badge/license-MIT%20License-brightgreen.svg)](https://opensource.org/licenses/MIT) + +Provision and manage [Grafana](https://github.com/grafana/grafana) - platform for analytics and monitoring + +## Requirements + +- Ansible >= 2.9 (It might work on previous versions, but we cannot guarantee it) +- libselinux-python on deployer host (only when deployer machine has SELinux) +- Grafana >= 5.1 (for older Grafana versions use this role in version 0.10.1 or earlier) +- jmespath on deployer machine. If you are using Ansible from a Python virtualenv, install *jmespath* to the same virtualenv via pip. + +## Role Variables + +All variables which can be overridden are stored in [defaults/main.yml](defaults/main.yml) file as well as in table below. + +| Name | Default Value | Description | +| -------------- | ------------- | -----------------------------------| +| `grafana_use_provisioning` | true | Use Grafana provisioning capability when possible (**grafana_version=latest will assume >= 5.0**). | +| `grafana_provisioning_synced` | false | Ensure no previously provisioned dashboards are kept if not referenced anymore. | +| `grafana_version` | latest | Grafana package version | +| `grafana_manage_repo` | true | Manage package repository (or don't) | +| `grafana_yum_repo` | https://packages.grafana.com/oss/rpm | Yum repository URL | +| `grafana_yum_key` | https://packages.grafana.com/gpg.key | Yum repository gpg key | +| `grafana_rhsm_subscription` | | rhsm subscription name (redhat subscription-manager) | +| `grafana_rhsm_repo` | | rhsm repository name (redhat subscription-manager) | +| `grafana_apt_repo` | deb https://packages.grafana.com/oss/deb stable main | Apt repository string | +| `grafana_apt_key` | https://packages.grafana.com/gpg.key | Apt repository gpg key | +| `grafana_instance` | {{ ansible_fqdn \| default(ansible_host) \| default(inventory_hostname) }} | Grafana instance name | +| `grafana_logs_dir` | /var/log/grafana | Path to logs directory | +| `grafana_data_dir` | /var/lib/grafana | Path to database directory | +| `grafana_address` | 0.0.0.0 | Address on which Grafana listens | +| `grafana_port` | 3000 | port on which Grafana listens | +| `grafana_cap_net_bind_service` | false | Enables the use of ports below 1024 without root privileges by leveraging the 'capabilities' of the linux kernel. read: http://man7.org/linux/man-pages/man7/capabilities.7.html | +| `grafana_url` | "http://{{ grafana_address }}:{{ grafana_port }}" | Full URL used to access Grafana from a web browser | +| `grafana_api_url` | "{{ grafana_url }}" | URL used for API calls in provisioning if different from public URL. See [this issue](https://github.com/cloudalchemy/ansible-grafana/issues/70). | +| `grafana_domain` | "{{ ansible_fqdn \| default(ansible_host) \| default('localhost') }}" | setting is only used in as a part of the `root_url` option. Useful when using GitHub or Google OAuth | +| `grafana_server` | { protocol: http, enforce_domain: false, socket: "", cert_key: "", cert_file: "", enable_gzip: false, static_root_path: public, router_logging: false } | [server](http://docs.grafana.org/installation/configuration/#server) configuration section | +| `grafana_security` | { admin_user: admin, admin_password: "" } | [security](http://docs.grafana.org/installation/configuration/#security) configuration section | +| `grafana_database` | { type: sqlite3 } | [database](http://docs.grafana.org/installation/configuration/#database) configuration section | +| `grafana_welcome_email_on_sign_up` | false | Send welcome email after signing up | +| `grafana_users` | { allow_sign_up: false, auto_assign_org_role: Viewer, default_theme: dark } | [users](http://docs.grafana.org/installation/configuration/#users) configuration section | +| `grafana_auth` | {} | [authorization](http://docs.grafana.org/installation/configuration/#auth) configuration section | +| `grafana_ldap` | {} | [ldap](http://docs.grafana.org/installation/ldap/) configuration section. group_mappings are expanded, see defaults for example | +| `grafana_session` | {} | [session](http://docs.grafana.org/installation/configuration/#session) management configuration section | +| `grafana_analytics` | {} | Google [analytics](http://docs.grafana.org/installation/configuration/#analytics) configuration section | +| `grafana_smtp` | {} | [smtp](http://docs.grafana.org/installation/configuration/#smtp) configuration section | +| `grafana_alerting` | {} | [alerting](http://docs.grafana.org/installation/configuration/#alerting) configuration section | +| `grafana_log` | {} | [log](http://docs.grafana.org/installation/configuration/#log) configuration section | +| `grafana_metrics` | {} | [metrics](http://docs.grafana.org/installation/configuration/#metrics) configuration section | +| `grafana_tracing` | {} | [tracing](http://docs.grafana.org/installation/configuration/#tracing) configuration section | +| `grafana_snapshots` | {} | [snapshots](http://docs.grafana.org/installation/configuration/#snapshots) configuration section | +| `grafana_image_storage` | {} | [image storage](http://docs.grafana.org/installation/configuration/#external-image-storage) configuration section | +| `grafana_dashboards` | [] | List of dashboards which should be imported | +| `grafana_dashboards_dir` | "dashboards" | Path to a local directory containing dashboards files in `json` format | +| `grafana_datasources` | [] | List of datasources which should be configured | +| `grafana_environment` | {} | Optional Environment param for Grafana installation, useful ie for setting http_proxy | +| `grafana_plugins` | [] | List of Grafana plugins which should be installed | +| `grafana_alert_notifications` | [] | List of alert notification channels to be created, updated, or deleted | + +Data source example: + +```yaml +grafana_datasources: + - name: prometheus + type: prometheus + access: proxy + url: 'http://{{ prometheus_web_listen_address }}' + basicAuth: false +``` + +Dashboard example: + +```yaml +grafana_dashboards: + - dashboard_id: 111 + revision_id: 1 + datasource: prometheus +``` + +Alert notification channel example: + +**NOTE**: setting the variable `grafana_alert_notifications` will only come into +effect when `grafana_use_provisioning` is `true`. That means the new +provisioning system using config files, which is available starting from Grafana +v5.0, needs to be in use. + +```yaml +grafana_alert_notifications: + notifiers: + - name: Channel 1 + type: email + uid: channel1 + is_default: false + send_reminder: false + settings: + addresses: "example@example.com" + autoResolve: true + delete_notifiers: + - name: Channel 2 + uid: channel2 +``` + +## Supported CPU Architectures + +Historically packages were taken from different channels according to CPU architecture. Specifically, armv6/armv7 and aarch64/arm64 packages were via [unofficial packages distributed by fg2it](https://github.com/fg2it/grafana-on-raspberry). Now that Grafana publishes official ARM builds, all packages are taken from the official [Debian/Ubuntu](http://docs.grafana.org/installation/debian/#installing-on-debian-ubuntu) or [RPM](http://docs.grafana.org/installation/rpm/) packages. + +## Example + +### Playbook + +Fill in the admin password field with your choice, the Grafana web page won't ask to change it at the first login. + +```yaml +- hosts: all + roles: + - role: grafana.grafana.grafana + vars: + grafana_security: + admin_user: admin + admin_password: enter_your_secure_password +``` + + +## Local Testing + +The preferred way of locally testing the role is to use Docker and [molecule](https://github.com/ansible-community/molecule). You will have to install Docker on your system. + +For more information about molecule go to their [docs](http://molecule.readthedocs.io/en/latest/). + +## License + +This project is licensed under MIT License. See [LICENSE](/LICENSE) for more details. + +## Credits +This role was migrated from [cloudalchemy.grafana](https://github.com/cloudalchemy/ansible-grafana). diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml new file mode 100644 index 0000000..7819308 --- /dev/null +++ b/roles/grafana/defaults/main.yml @@ -0,0 +1,262 @@ +--- +grafana_version: latest +grafana_manage_repo: true + +grafana_yum_repo: "https://packages.grafana.com/oss/rpm" +grafana_yum_key: "https://packages.grafana.com/gpg.key" + +grafana_rhsm_subscription: "" +grafana_rhsm_repo: "" + +grafana_apt_repo: "deb https://packages.grafana.com/oss/deb stable main" +grafana_apt_key: "https://packages.grafana.com/gpg.key" + +# Should we use the provisioning capability when possible (provisioning require grafana >= 5.0) +grafana_use_provisioning: true + +# Should the provisioning be kept synced. If true, previous provisioned objects will be removed if not referenced anymore. +grafana_provisioning_synced: false + +grafana_instance: "{{ ansible_fqdn | default(ansible_host) | default(inventory_hostname) }}" + +grafana_logs_dir: "/var/log/grafana" +grafana_data_dir: "/var/lib/grafana" + +grafana_address: "0.0.0.0" +grafana_port: 3000 +# To enable the use of ports below 1024 for unprivileged processes linux needs to set CAP_NET_BIND_SERVICE. +# This has some security implications, and should be a conscious choice. +# Get informed by reading: http://man7.org/linux/man-pages/man7/capabilities.7.html +grafana_cap_net_bind_service: false + +# External Grafana address. Variable maps to "root_url" in grafana server section +grafana_url: "http://{{ grafana_address }}:{{ grafana_port }}" +grafana_api_url: "{{ grafana_url }}" +grafana_domain: "{{ ansible_fqdn | default(ansible_host) | default('localhost') }}" + +# Additional options for grafana "server" section +# This section WILL omit options for: http_addr, http_port, domain, and root_url, as those settings are set by variables listed before +grafana_server: + protocol: http + enforce_domain: false + socket: "" + cert_key: "" + cert_file: "" + enable_gzip: false + static_root_path: public + router_logging: false + serve_from_sub_path: false + +# Variables correspond to ones in grafana.ini configuration file +# Security +grafana_security: + admin_user: admin + admin_password: "" +# secret_key: "" +# login_remember_days: 7 +# cookie_username: grafana_user +# cookie_remember_name: grafana_remember +# disable_gravatar: true +# data_source_proxy_whitelist: + +# Database setup +grafana_database: + type: sqlite3 +# host: 127.0.0.1:3306 +# name: grafana +# user: root +# password: "" +# url: "" +# ssl_mode: disable +# path: grafana.db +# max_idle_conn: 2 +# max_open_conn: "" +# log_queries: "" + +# Remote cache +grafana_remote_cache: {} + +# User management and registration +grafana_welcome_email_on_sign_up: false +grafana_users: + allow_sign_up: false + # allow_org_create: true + # auto_assign_org: true + auto_assign_org_role: Viewer + # login_hint: "email or username" + default_theme: dark + # external_manage_link_url: "" + # external_manage_link_name: "" + # external_manage_info: "" + +# grafana authentication mechanisms +grafana_auth: {} +# disable_login_form: false +# oauth_auto_login: false +# disable_signout_menu: false +# signout_redirect_url: "" +# anonymous: +# org_name: "Main Organization" +# org_role: Viewer +# ldap: +# config_file: "/etc/grafana/ldap.toml" +# allow_sign_up: false +# basic: +# enabled: true + +grafana_ldap: {} +# verbose_logging: false +# servers: +# host: 127.0.0.1 +# port: 389 # 636 for SSL +# use_ssl: false +# start_tls: false +# ssl_skip_verify: false +# root_ca_cert: /path/to/certificate.crt +# bind_dn: "cn=admin,dc=grafana,dc=org" +# bind_password: grafana +# search_filter: "(cn=%s)" # "(sAMAccountName=%s)" on AD +# search_base_dns: +# - "dc=grafana,dc=org" +# group_search_filter: "(&(objectClass=posixGroup)(memberUid=%s))" +# group_search_base_dns: +# - "ou=groups,dc=grafana,dc=org" +# attributes: +# name: givenName +# surname: sn +# username: sAMAccountName +# member_of: memberOf +# email: mail +# group_mappings: +# - name: Main Org. +# id: 1 +# groups: +# - group_dn: "cn=admins,ou=groups,dc=grafana,dc=org" +# org_role: Admin +# - group_dn: "cn=editors,ou=groups,dc=grafana,dc=org" +# org_role: Editor +# - group_dn: "*" +# org_role: Viewer +# - name: Alternative Org +# id: 2 +# groups: +# - group_dn: "cn=alternative_admins,ou=groups,dc=grafana,dc=org" +# org_role: Admin + +grafana_session: {} +# provider: file +# provider_config: "sessions" + +grafana_analytics: {} +# reporting_enabled: true +# google_analytics_ua_id: "" + +# Set this for mail notifications +grafana_smtp: {} +# host: +# user: +# password: +# from_address: + +# Enable grafana alerting mechanism +grafana_alerting: + execute_alerts: true +# error_or_timeout: 'alerting' +# nodata_or_nullvalues: 'no_data' +# concurrent_render_limit: 5 + +# Grafana logging configuration +grafana_log: +# mode: 'console file' +# level: info + +# Internal grafana metrics system +grafana_metrics: {} +# interval_seconds: 10 +# graphite: +# address: "localhost:2003" +# prefix: "prod.grafana.%(instance_name)s" + +# Distributed tracing options +grafana_tracing: {} +# address: "localhost:6831" +# always_included_tag: "tag1:value1,tag2:value2" +# sampler_type: const +# sampler_param: 1 + +grafana_snapshots: {} +# external_enabled: true +# external_snapshot_url: "https://snapshots-origin.raintank.io" +# external_snapshot_name: "Publish to snapshot.raintank.io" +# snapshot_remove_expired: true +# snapshot_TTL_days: 90 + +# External image store +grafana_image_storage: {} +# provider: gcs +# key_file: +# bucket: +# path: + + +####### +# Plugins from https://grafana.com/plugins +grafana_plugins: [] +# - raintank-worldping-app + +# Dashboards from https://grafana.com/dashboards +grafana_dashboards: [] +# - dashboard_id: '4271' +# revision_id: '3' +# datasource: 'Prometheus' +# - dashboard_id: '1860' +# revision_id: '4' +# datasource: 'Prometheus' +# - dashboard_id: '358' +# revision_id: '1' +# datasource: 'Prometheus' + +grafana_dashboards_dir: "dashboards" + +# Alert notification channels to configure +grafana_alert_notifications: [] +# - name: "Email Alert" +# type: "email" +# uid: channel1 +# is_default: true +# settings: +# addresses: "example@example.com" + +# Datasources to configure +grafana_datasources: [] +# - name: "Prometheus" +# type: "prometheus" +# access: "proxy" +# url: "http://prometheus.mydomain" +# basicAuth: true +# basicAuthUser: "admin" +# basicAuthPassword: "password" +# isDefault: true +# jsonData: +# tlsAuth: false +# tlsAuthWithCACert: false +# tlsSkipVerify: true + +# API keys to configure +grafana_api_keys: [] +# - name: "admin" +# role: "Admin" +# - name: "viewer" +# role: "Viewer" +# - name: "editor" +# role: "Editor" + +# The location where the keys should be stored. +grafana_api_keys_dir: "{{ lookup('env', 'HOME') }}/grafana/keys" + +grafana_environment: {} + +# Panels configurations +grafana_panels: {} +# disable_sanitize_html: false +# enable_alpha: false diff --git a/roles/grafana/handlers/main.yml b/roles/grafana/handlers/main.yml new file mode 100644 index 0000000..d457b42 --- /dev/null +++ b/roles/grafana/handlers/main.yml @@ -0,0 +1,28 @@ +--- +- name: "Restart grafana" + ansible.builtin.service: + name: grafana-server + state: restarted + become: true + listen: "restart_grafana" + tags: + - grafana_run + +- name: "Set privileges on provisioned dashboards" + ansible.builtin.file: + path: "{{ grafana_data_dir }}/dashboards" + recurse: true + owner: "grafana" + group: "grafana" + mode: "0640" + become: true + listen: "provisioned dashboards changed" + +- name: "Set privileges on provisioned dashboards directory" + ansible.builtin.file: + path: "{{ grafana_data_dir }}/dashboards" + state: "directory" + recurse: false + mode: "0755" + become: true + listen: "provisioned dashboards changed" diff --git a/roles/grafana/meta/main.yml b/roles/grafana/meta/main.yml new file mode 100644 index 0000000..564b8bc --- /dev/null +++ b/roles/grafana/meta/main.yml @@ -0,0 +1,31 @@ +--- +galaxy_info: + author: "Grafana" + description: "Grafana - platform for analytics and monitoring" + license: "MIT" + min_ansible_version: "2.9" + platforms: + - name: Ubuntu + versions: + - bionic + - xenial + - name: Debian + versions: + - stretch + - buster + - name: EL + versions: + - "7" + - "8" + - name: Fedora + versions: + - "30" + - "31" + galaxy_tags: + - grafana + - dashboard + - alerts + - alerting + - presentation + - monitoring + - metrics diff --git a/roles/grafana/molecule/alternative/converge.yml b/roles/grafana/molecule/alternative/converge.yml new file mode 100644 index 0000000..56e1ba8 --- /dev/null +++ b/roles/grafana/molecule/alternative/converge.yml @@ -0,0 +1,105 @@ +--- +- name: "Run role" + hosts: all + any_errors_fatal: true + roles: + - grafana.grafana.grafana + vars: + grafana_version: 6.2.5 + grafana_security: + admin_user: admin + admin_password: "password" + grafana_address: "127.0.0.1" + grafana_auth: + login_maximum_inactive_lifetime_days: 42 + disable_login_form: false + oauth_auto_login: false + disable_signout_menu: false + signout_redirect_url: "" + anonymous: + org_name: "Main Organization" + org_role: Viewer + ldap: + config_file: "/etc/grafana/ldap.toml" + allow_sign_up: false + basic: + enabled: true + grafana_ldap: + verbose_logging: false + servers: + host: 127.0.0.1 + port: 389 + use_ssl: false + start_tls: false + ssl_skip_verify: false + root_ca_cert: /path/to/certificate.crt + bind_dn: "cn=admin,dc=grafana,dc=org" + bind_password: grafana + search_filter: "(cn=%s)" + search_base_dns: + - "dc=grafana,dc=org" + group_search_filter: "(&(objectClass=posixGroup)(memberUid=%s))" + group_search_base_dns: + - "ou=groups,dc=grafana,dc=org" + attributes: + name: givenName + surname: sn + username: sAMAccountName + member_of: memberOf + email: mail + group_mappings: + - name: "Main Organization" + id: 1 + groups: + - group_dn: "cn=admins,ou=groups,dc=grafana,dc=org" + org_role: Admin + - group_dn: "cn=editors,ou=groups,dc=grafana,dc=org" + org_role: Editor + - group_dn: "*" + org_role: Viewer + - name: "Alternative Org" + id: 2 + groups: + - group_dn: "cn=alternative_admins,ou=groups,dc=grafana,dc=org" + org_role: Admin + grafana_api_keys: + - name: "admin" + role: "Admin" + - name: "viewer" + role: "Viewer" + - name: "editor" + role: "Editor" + grafana_api_keys_dir: "/tmp/grafana/keys" + grafana_plugins: + - raintank-worldping-app + grafana_alert_notifications: + notifiers: + - name: "Email Alert" + type: "email" + uid: notifier1 + is_default: true + settings: + addresses: "example@example.com" + grafana_dashboards: + - dashboard_id: '1860' + revision_id: '4' + datasource: 'Prometheus' + - dashboard_id: '358' + revision_id: '1' + datasource: 'Prometheus' + grafana_datasources: + - name: "Prometheus" + type: "prometheus" + access: "proxy" + url: "http://prometheus.mydomain" + basicAuth: true + basicAuthUser: "admin" + basicAuthPassword: "password" + isDefault: true + jsonData: + tlsAuth: false + tlsAuthWithCACert: false + tlsSkipVerify: true + grafana_log: + mode: syslog + level: warn diff --git a/roles/grafana/molecule/alternative/molecule.yml b/roles/grafana/molecule/alternative/molecule.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/grafana/molecule/alternative/molecule.yml @@ -0,0 +1 @@ +--- diff --git a/roles/grafana/molecule/alternative/tests/test_alternative.py b/roles/grafana/molecule/alternative/tests/test_alternative.py new file mode 100644 index 0000000..cf6476b --- /dev/null +++ b/roles/grafana/molecule/alternative/tests/test_alternative.py @@ -0,0 +1,52 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') + + +def test_directories(host): + dirs = [ + "/etc/grafana", + "/var/log/grafana", + "/var/lib/grafana", + "/var/lib/grafana/dashboards", + "/var/lib/grafana/plugins", + "/var/lib/grafana/plugins/raintank-worldping-app" + ] + files = [ + "/etc/grafana/grafana.ini", + "/etc/grafana/ldap.toml" + ] + for directory in dirs: + d = host.file(directory) + assert d.is_directory + assert d.exists + for file in files: + f = host.file(file) + assert f.exists + assert f.is_file + + +def test_service(host): + s = host.service("grafana-server") + # assert s.is_enabled + assert s.is_running + + +def test_packages(host): + p = host.package("grafana") + assert p.is_installed + assert p.version == "6.2.5" + + +def test_socket(host): + assert host.socket("tcp://127.0.0.1:3000").is_listening + + +def test_custom_auth_option(host): + f = host.file("/etc/grafana/grafana.ini") + assert f.contains("login_maximum_inactive_lifetime_days = 42") diff --git a/roles/grafana/molecule/default/converge.yml b/roles/grafana/molecule/default/converge.yml new file mode 100644 index 0000000..3d76ca1 --- /dev/null +++ b/roles/grafana/molecule/default/converge.yml @@ -0,0 +1,10 @@ +--- +- name: "Run role" + hosts: all + any_errors_fatal: true + roles: + - grafana.grafana.grafana + vars: + grafana_security: + admin_user: "admin" + admin_password: "password" diff --git a/roles/grafana/molecule/default/molecule.yml b/roles/grafana/molecule/default/molecule.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/grafana/molecule/default/molecule.yml @@ -0,0 +1 @@ +--- diff --git a/roles/grafana/molecule/default/tests/test_default.py b/roles/grafana/molecule/default/tests/test_default.py new file mode 100644 index 0000000..67363a3 --- /dev/null +++ b/roles/grafana/molecule/default/tests/test_default.py @@ -0,0 +1,50 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all') + + +def test_directories(host): + dirs = [ + "/etc/grafana", + "/var/log/grafana", + "/var/lib/grafana", + "/var/lib/grafana/dashboards", + "/var/lib/grafana/plugins" + ] + files = [ + "/etc/grafana/grafana.ini" + ] + for directory in dirs: + d = host.file(directory) + assert d.is_directory + assert d.exists + for file in files: + f = host.file(file) + assert f.exists + assert f.is_file + + +def test_service(host): + s = host.service("grafana-server") + # assert s.is_enabled + assert s.is_running + + +def test_packages(host): + p = host.package("grafana") + assert p.is_installed + + +def test_socket(host): + assert host.socket("tcp://0.0.0.0:3000").is_listening + + +def test_yum_repo(host): + if host.system_info.distribution in ['centos', 'redhat', 'fedora']: + f = host.file("/etc/yum.repos.d/grafana.repo") + assert f.exists diff --git a/roles/grafana/tasks/api_keys.yml b/roles/grafana/tasks/api_keys.yml new file mode 100644 index 0000000..d6ca94b --- /dev/null +++ b/roles/grafana/tasks/api_keys.yml @@ -0,0 +1,43 @@ +--- +- name: "Ensure grafana key directory exists" + ansible.builtin.file: + path: "{{ grafana_api_keys_dir }}/{{ inventory_hostname }}" + state: directory + mode: "0755" + become: false + delegate_to: localhost + +- name: "Check api key list" + ansible.builtin.uri: + url: "{{ grafana_api_url }}/api/auth/keys" + user: "{{ grafana_security.admin_user }}" + password: "{{ grafana_security.admin_password }}" + force_basic_auth: true + return_content: true + register: __existing_api_keys + no_log: "{{ 'false' if lookup('env', 'CI') else 'true' }}" + +- name: "Create grafana api keys" + ansible.builtin.uri: + url: "{{ grafana_api_url }}/api/auth/keys" + user: "{{ grafana_security.admin_user }}" + password: "{{ grafana_security.admin_password }}" + force_basic_auth: true + method: POST + body_format: json + body: "{{ item | to_json }}" + loop: "{{ grafana_api_keys }}" + register: __new_api_keys + no_log: "{{ 'false' if lookup('env', 'CI') else 'true' }}" + when: "((__existing_api_keys['json'] | selectattr('name', 'equalto', item['name'])) | list) | length == 0" + +- name: "Create api keys file to allow the keys to be seen and used by other automation" + ansible.builtin.copy: + dest: "{{ grafana_api_keys_dir }}/{{ inventory_hostname }}/{{ item['item']['name'] }}.key" + content: "{{ item['json']['key'] }}" + backup: false + mode: "0644" + loop: "{{ __new_api_keys['results'] }}" + become: false + delegate_to: localhost + when: "item['json'] is defined" diff --git a/roles/grafana/tasks/configure.yml b/roles/grafana/tasks/configure.yml new file mode 100644 index 0000000..cf73203 --- /dev/null +++ b/roles/grafana/tasks/configure.yml @@ -0,0 +1,84 @@ +--- +- name: "Ensure grafana directories exist" + ansible.builtin.file: + path: "{{ item.path }}" + state: "directory" + owner: "{{ item.owner | default('root') }}" + group: "{{ item.group | default('grafana') }}" + mode: "{{ item.mode | default('0755') }}" + loop: + - path: "/etc/grafana" + - path: "/etc/grafana/datasources" + - path: "/etc/grafana/provisioning" + - path: "/etc/grafana/provisioning/datasources" + - path: "/etc/grafana/provisioning/dashboards" + - path: "/etc/grafana/provisioning/notifiers" + - path: "/etc/grafana/provisioning/plugins" + - path: "{{ grafana_logs_dir }}" + owner: grafana + - path: "{{ grafana_data_dir }}" + owner: grafana + - path: "{{ grafana_data_dir }}/dashboards" + owner: grafana + - path: "{{ grafana_data_dir }}/plugins" + owner: grafana + +- name: "Create grafana main configuration file" + ansible.builtin.template: + src: "grafana.ini.j2" + dest: "/etc/grafana/grafana.ini" + owner: "root" + group: "grafana" + mode: "0640" + no_log: "{{ 'false' if lookup('env', 'CI') else 'true' }}" + notify: restart_grafana + +- name: "Create grafana LDAP configuration file" + ansible.builtin.template: + src: "ldap.toml.j2" + dest: "{{ grafana_auth.ldap.config_file | default('/etc/grafana/ldap.toml') }}" + owner: "root" + group: "grafana" + mode: "0640" + no_log: "{{ 'false' if lookup('env', 'CI') else 'true' }}" + notify: restart_grafana + when: + - "'ldap' in grafana_auth" + - "'enabled' not in grafana_auth.ldap or grafana_auth.ldap.enabled" + +- name: "Enable grafana socket" + when: + - "grafana_server.protocol is defined and grafana_server.protocol == 'socket'" + - "grafana_server.socket | dirname != '/var/run'" + block: + - name: "Create grafana socket directory" + ansible.builtin.file: + path: "{{ grafana_server.socket | dirname }}" + state: "directory" + mode: "0775" + owner: "grafana" + group: "grafana" + + - name: "Ensure grafana socket directory created on startup" + ansible.builtin.template: + src: "tmpfiles.j2" + dest: "/etc/tmpfiles.d/grafana.conf" + owner: "root" + group: "root" + mode: "0644" + +- name: "Enable grafana to ports lower than port 1024" + community.general.capabilities: + path: /usr/sbin/grafana-server + capability: CAP_NET_BIND_SERVICE+ep + state: present + when: + - "grafana_port | int <= 1024" + - "grafana_cap_net_bind_service" + +- name: "Enable and start Grafana systemd unit" + ansible.builtin.systemd: + name: "grafana-server" + enabled: true + state: started + daemon_reload: true diff --git a/roles/grafana/tasks/dashboards.yml b/roles/grafana/tasks/dashboards.yml new file mode 100644 index 0000000..e2710a2 --- /dev/null +++ b/roles/grafana/tasks/dashboards.yml @@ -0,0 +1,133 @@ +--- +- name: "Download grafana.net dashboards" + become: false + delegate_to: localhost + run_once: true + when: "grafana_dashboards | length > 0" + block: + - name: "Create local grafana dashboard directory" + ansible.builtin.tempfile: + state: directory + register: __tmp_dashboards + changed_when: false + + - name: "Download grafana dashboard from grafana.net to local directory" + ansible.builtin.get_url: + url: "https://grafana.com/api/dashboards/{{ item.dashboard_id }}/revisions/{{ item.revision_id }}/download" + dest: "{{ __tmp_dashboards.path }}/{{ item.dashboard_id }}.json" + mode: "0644" + register: __download_dashboards + until: "__download_dashboards is succeeded" + retries: 5 + delay: 2 + changed_when: false + loop: "{{ grafana_dashboards }}" + + # As noted in [1] an exported dashboard replaces the exporter's datasource + # name with a representative name, something like 'DS_GRAPHITE'. The name + # is different for each datasource plugin, but always begins with 'DS_'. + # In the rest of the data, the same name is used, but captured in braces, + # for example: '${DS_GRAPHITE}'. + # + # [1] http://docs.grafana.org/reference/export_import/#import-sharing-with-grafana-2-x-or-3-0 + # + # The data structure looks (massively abbreviated) something like: + # + # "name": "DS_GRAPHITE", + # "datasource": "${DS_GRAPHITE}", + # + # If we import the downloaded dashboard verbatim, it will not automatically + # be connected to the data source like we want it. The Grafana UI expects + # us to do the final connection by hand, which we do not want to do. + # So, in the below task we ensure that we replace instances of this string + # with the data source name we want. + # To make sure that we're not being too greedy with the regex replacement + # of the data source to use for each dashboard that's uploaded, we make the + # regex match very specific by using the following: + # + # 1. Literal boundaries for " on either side of the match. + # 2. Non-capturing optional group matches for the ${} bits which may, or + # or may not, be there.. + # 3. A case-sensitive literal match for DS . + # 4. A one-or-more case-sensitive match for the part that follows the + # underscore, with only A-Z, 0-9 and - or _ allowed. + # + # This regex can be tested and understood better by looking at the + # matches and non-matches in https://regex101.com/r/f4Gkvg/6 + + - name: "Set the correct data source name in the dashboard" + ansible.builtin.replace: + dest: "{{ __tmp_dashboards.path }}/{{ item.dashboard_id }}.json" + regexp: '"(?:\${)?DS_[A-Z0-9_-]+(?:})?"' + replace: '"{{ item.datasource }}"' + changed_when: false + loop: "{{ grafana_dashboards }}" + +- name: "Import grafana dashboards via api" + community.grafana.grafana_dashboard: + grafana_url: "{{ grafana_api_url }}" + grafana_user: "{{ grafana_security.admin_user }}" + grafana_password: "{{ grafana_security.admin_password }}" + path: "{{ item }}" + message: "Updated by ansible role {{ ansible_role_name }}" + state: present + overwrite: true + no_log: "{{ 'false' if lookup('env', 'CI') else 'true' }}" + with_fileglob: + - "{{ __tmp_dashboards.path }}/*" + - "{{ grafana_dashboards_dir }}/*.json" + when: "not grafana_use_provisioning" + +- name: "Import grafana dashboards through provisioning" + when: grafana_use_provisioning + block: + - name: "Create/Update dashboards file (provisioning)" + ansible.builtin.copy: + dest: "/etc/grafana/provisioning/dashboards/ansible.yml" + content: | + apiVersion: 1 + providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + options: + path: "{{ grafana_data_dir }}/dashboards" + backup: false + owner: root + group: grafana + mode: "0640" + become: true + notify: restart_grafana + + - name: "Register previously copied dashboards" + ansible.builtin.find: + paths: "{{ grafana_data_dir }}/dashboards" + hidden: true + patterns: + - "*.json" + register: __dashboards_present + when: grafana_provisioning_synced + + - name: "Import grafana dashboards" + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ grafana_data_dir }}/dashboards/{{ item | basename }}" + mode: "0640" + with_fileglob: + - "{{ __tmp_dashboards.path }}/*" + - "{{ grafana_dashboards_dir }}/*.json" + become: true + register: __dashboards_copied + notify: "provisioned dashboards changed" + + - name: "Remove dashboards not present on deployer machine (synchronize)" + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: "{{ __dashboards_present_list | difference(__dashboards_copied_list) }}" + become: true + when: grafana_provisioning_synced + vars: + __dashboards_present_list: "{{ __dashboards_present | json_query('files[*].path') | default([]) }}" + __dashboards_copied_list: "{{ __dashboards_copied | json_query('results[*].dest') | default([]) }}" diff --git a/roles/grafana/tasks/datasources.yml b/roles/grafana/tasks/datasources.yml new file mode 100644 index 0000000..024ddca --- /dev/null +++ b/roles/grafana/tasks/datasources.yml @@ -0,0 +1,40 @@ +--- +- name: "Ensure datasources exist (via API)" + community.grafana.grafana_datasource: + grafana_url: "{{ grafana_api_url }}" + grafana_user: "{{ grafana_security.admin_user }}" + grafana_password: "{{ grafana_security.admin_password }}" + name: "{{ item.name }}" + url: "{{ item.url }}" + ds_type: "{{ item.type }}" + access: "{{ item.access | default(omit) }}" + is_default: "{{ item.isDefault | default(omit) }}" + basic_auth_user: "{{ item.basicAuthUser | default(omit) }}" + basic_auth_password: "{{ item.basicAuthPassword | default(omit) }}" + database: "{{ item.database | default(omit) }}" + user: "{{ item.user | default(omit) }}" + password: "{{ item.password | default(omit) }}" + aws_auth_type: "{{ item.aws_auth_type | default(omit) }}" + aws_default_region: "{{ item.aws_default_region | default(omit) }}" + aws_access_key: "{{ item.aws_access_key | default(omit) }}" + aws_secret_key: "{{ item.aws_secret_key | default(omit) }}" + aws_credentials_profile: "{{ item.aws_credentials_profile | default(omit) }}" + aws_custom_metrics_namespaces: "{{ item.aws_custom_metrics_namespaces | default(omit) }}" + loop: "{{ grafana_datasources }}" + when: "not grafana_use_provisioning" + +- name: "Create/Update datasources file (provisioning)" + ansible.builtin.copy: + dest: "/etc/grafana/provisioning/datasources/ansible.yml" + content: | + apiVersion: 1 + deleteDatasources: [] + datasources: + "{{ grafana_datasources | to_nice_yaml }}" + backup: false + owner: root + group: grafana + mode: 0640 + notify: restart_grafana + become: true + when: "grafana_use_provisioning" diff --git a/roles/grafana/tasks/install.yml b/roles/grafana/tasks/install.yml new file mode 100644 index 0000000..4b935db --- /dev/null +++ b/roles/grafana/tasks/install.yml @@ -0,0 +1,89 @@ +--- +- name: "Remove conflicting grafana packages" + ansible.builtin.package: + name: grafana-data + state: absent + +- name: "Install dependencies" + ansible.builtin.package: + name: "{{ _grafana_dependencies }}" + state: present + update_cache: true + when: "(_grafana_dependencies | default())" + +- name: "Prepare yum/dnf" + when: + - "ansible_pkg_mgr in ['yum', 'dnf']" + - "(grafana_manage_repo)" + environment: "{{ grafana_environment }}" + block: + - name: "Add Grafana yum/dnf repository" + ansible.builtin.yum_repository: + name: grafana + description: grafana + baseurl: "{{ grafana_yum_repo }}" + enabled: true + gpgkey: "{{ grafana_yum_key | default(omit) }}" + repo_gpgcheck: "{{ true if (grafana_yum_key) else omit }}" + gpgcheck: "{{ true if (grafana_yum_key) else omit }}" + when: "(not grafana_rhsm_repo)" + + - name: "Attach RHSM subscription" + when: "(grafana_rhsm_subscription)" + block: + - name: "Check if Grafana RHSM subscription is enabled" + ansible.builtin.command: + cmd: "subscription-manager list --consumed --matches={{ grafana_rhsm_subscription | quote }} --pool-only" + register: __subscription_manager_consumed + changed_when: false + when: (grafana_rhsm_subscription) + + - name: "Find RHSM repo subscription pool id" + ansible.builtin.command: + cmd: "subscription-manager list --available --matches={{ grafana_rhsm_subscription | quote }} --pool-only" + register: __subscription_manager_available + changed_when: false + when: + - "(grafana_rhsm_subscription)" + - "__subscription_manager_consumed.stdout | length <= 0" + + - name: "Attach RHSM subscription" + ansible.builtin.command: + cmd: "subscription-manager attach --pool={{ __subscription_manager_available.stdout }}" + register: __subscription_manager_attach + changed_when: "__subscription_manager_attach.stdout is search('Successfully attached a subscription')" + failed_when: "__subscription_manager_attach.stdout is search('could not be found')" + when: + - "(grafana_rhsm_subscription)" + - "__subscription_manager_consumed.stdout | default() | length <= 0" + - "__subscription_manager_available.stdout | default() | length > 0" + + - name: "Enable RHSM repository" + community.general.rhsm_repository: + name: "{{ grafana_rhsm_repo }}" + state: enabled + when: (grafana_rhsm_repo) + +- name: "Prepare apt" + when: + - "ansible_pkg_mgr == 'apt'" + - "(grafana_manage_repo)" + environment: "{{ grafana_environment }}" + block: + - name: "Import Grafana apt gpg key" + ansible.builtin.apt_key: + url: "{{ grafana_apt_key }}" + state: present + validate_certs: false + + - name: "Add Grafana apt repository" + ansible.builtin.apt_repository: + repo: "{{ grafana_apt_repo }}" + state: present + update_cache: true + +- name: "Install Grafana" + ansible.builtin.package: + name: "{{ grafana_package }}" + state: "{{ (grafana_version == 'latest') | ternary('latest', 'present') }}" + notify: restart_grafana diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml new file mode 100644 index 0000000..e79c7c6 --- /dev/null +++ b/roles/grafana/tasks/main.yml @@ -0,0 +1,120 @@ +--- +- name: "Gather variables for each operating system" + ansible.builtin.include_vars: "{{ distrovars }}" + vars: + distrovars: "{{ lookup('first_found', params, errors='ignore') }}" + params: + skip: true + files: + - "{{ ansible_distribution | lower }}-{{ ansible_distribution_version | lower }}.yml" + - "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_os_family | lower }}-{{ ansible_distribution_major_version | lower }}.yml" + - "{{ ansible_distribution | lower }}.yml" + - "{{ ansible_os_family | lower }}.yml" + paths: + - "vars/distro" + tags: + - grafana_install + - grafana_configure + - grafana_datasources + - grafana_notifications + - grafana_dashboards + +- name: Preflight + ansible.builtin.include_tasks: + file: preflight.yml + apply: + tags: + - grafana_install + - grafana_configure + - grafana_datasources + - grafana_notifications + - grafana_dashboards + +- name: Install + ansible.builtin.include_tasks: + file: install.yml + apply: + become: true + tags: + - grafana_install + +- name: Configure + ansible.builtin.include_tasks: + file: configure.yml + apply: + become: true + tags: + - grafana_configure + +- name: Plugins + ansible.builtin.include_tasks: + file: plugins.yml + apply: + tags: + - grafana_configure + when: "grafana_plugins != []" + +- name: "Restart grafana before configuring datasources and dashboards" + ansible.builtin.meta: flush_handlers + tags: + - grafana_install + - grafana_configure + - grafana_datasources + - grafana_notifications + - grafana_dashboards + - grafana_run + +- name: "Wait for grafana to start" + ansible.builtin.wait_for: + host: "{{ grafana_address if grafana_server.protocol is undefined or grafana_server.protocol in ['http', 'https'] else omit }}" + port: "{{ grafana_port if grafana_server.protocol is undefined or grafana_server.protocol in ['http', 'https'] else omit }}" + path: "{{ grafana_server.socket | default() if grafana_server.protocol is defined and grafana_server.protocol == 'socket' else omit }}" + tags: + - grafana_install + - grafana_configure + - grafana_datasources + - grafana_notifications + - grafana_dashboards + - grafana_run + +- name: "Api keys" + ansible.builtin.include_tasks: + file: api_keys.yml + apply: + tags: + - grafana_configure + - grafana_run + when: "grafana_api_keys | length > 0" + +- name: Datasources + ansible.builtin.include_tasks: + file: datasources.yml + apply: + tags: + - grafana_configure + - grafana_datasources + - grafana_run + when: "grafana_datasources != []" + +- name: Notifications + ansible.builtin.include_tasks: + file: notifications.yml + apply: + tags: + - grafana_configure + - grafana_notifications + - grafana_run + when: "grafana_alert_notifications | length > 0" + +- name: Dashboards + ansible.builtin.include_tasks: + file: dashboards.yml + apply: + tags: + - grafana_configure + - grafana_dashboards + - grafana_run + when: "grafana_dashboards | length > 0 or __found_dashboards | length > 0" + vars: + __found_dashboards: "{{ lookup('fileglob', grafana_dashboards_dir + '/*.json', wantlist=True) }}" diff --git a/roles/grafana/tasks/notifications.yml b/roles/grafana/tasks/notifications.yml new file mode 100644 index 0000000..4bbac3f --- /dev/null +++ b/roles/grafana/tasks/notifications.yml @@ -0,0 +1,13 @@ +--- +- name: "Create/Delete/Update alert notifications channels (provisioning)" + ansible.builtin.copy: + content: | + apiVersion: 1 + "{{ grafana_alert_notifications | to_nice_yaml }}" + dest: /etc/grafana/provisioning/notifiers/ansible.yml + owner: root + group: grafana + mode: 0640 + become: true + notify: restart_grafana + when: grafana_use_provisioning diff --git a/roles/grafana/tasks/plugins.yml b/roles/grafana/tasks/plugins.yml new file mode 100644 index 0000000..133624a --- /dev/null +++ b/roles/grafana/tasks/plugins.yml @@ -0,0 +1,20 @@ +--- +- name: "Check which plugins are installed" + ansible.builtin.find: + file_type: directory + recurse: false + paths: "{{ grafana_data_dir }}/plugins" + register: __installed_plugins + +- name: "Install plugins" + become: true + ansible.builtin.command: + cmd: "grafana-cli --pluginsDir {{ grafana_data_dir }}/plugins plugins install {{ item }}" + creates: "{{ grafana_data_dir }}/plugins/{{ item }}" + loop: "{{ grafana_plugins | difference(__installed_plugins.files) }}" + register: __plugin_install + until: "__plugin_install is succeeded" + retries: 5 + delay: 2 + notify: + - restart_grafana diff --git a/roles/grafana/tasks/preflight.yml b/roles/grafana/tasks/preflight.yml new file mode 100644 index 0000000..d209abc --- /dev/null +++ b/roles/grafana/tasks/preflight.yml @@ -0,0 +1,84 @@ +--- +- name: "Check variable types" + ansible.builtin.assert: + that: + - "grafana_server is mapping" + - "grafana_database is mapping" + - "grafana_security is mapping" + +- name: "Fail when datasources aren't configured when dashboards are set to be installed" + ansible.builtin.fail: + msg: "You need to specify datasources for dashboards!!!" + when: "grafana_dashboards != [] and grafana_datasources == []" + +- name: "Fail when grafana admin user isn't set" + ansible.builtin.fail: + msg: "Please specify grafana admin user (grafana_security.admin_user)" + when: + - "grafana_security.admin_user == '' or + grafana_security.admin_user is not defined" + +- name: "Fail when grafana admin password isn't set" + ansible.builtin.fail: + msg: "Please specify grafana admin password (grafana_security.admin_password)" + when: + - "grafana_security.admin_password == '' or + grafana_security.admin_password is not defined" + +- name: "Fail on incorrect variable types in datasource definitions" + ansible.builtin.fail: + msg: "Boolean variables in grafana_datasources shouldn't be passed as strings. Please remove unneeded apostrophes." + when: "( item.isDefault is defined and item.isDefault is string ) or + ( item.basicAuth is defined and item.basicAuth is string )" + loop: "{{ grafana_datasources }}" + +- name: "Fail on bad database configuration" + ansible.builtin.fail: + msg: "Wrong database configuration. Please look at http://docs.grafana.org/installation/configuration/#database" + when: "( grafana_database.type == 'sqlite3' and grafana_database.url is defined ) or + ( grafana_database.type != 'sqlite3' and grafana_database.path is defined ) or + ( grafana_database.type == 'sqlite3' and grafana_database.host is defined ) or + ( grafana_database.type == 'sqlite3' and grafana_database.user is defined ) or + ( grafana_database.type == 'sqlite3' and grafana_database.password is defined ) or + ( grafana_database.type == 'sqlite3' and grafana_database.server_cert_name is defined )" + +- name: "Fail when grafana domain isn't properly configured" + ansible.builtin.fail: + msg: "Check server configuration. Please look at http://docs.grafana.org/installation/configuration/#server" + when: + - "grafana_server.root_url is defined" + - "grafana_server.root_url is search(grafana_server.domain)" + +- name: "Fail when grafana_api_keys uses invalid role names" + ansible.builtin.fail: + msg: "Check grafana_api_keys. The role can only be one of the following values: Viewer, Editor or Admin." + when: "item.role not in ['Viewer', 'Editor', 'Admin']" + loop: "{{ grafana_api_keys }}" + +- name: "Fail when grafana_ldap isn't set when grafana_auth.ldap is" + ansible.builtin.fail: + msg: "You need to configure grafana_ldap.servers and grafana_ldap.group_mappings when grafana_auth.ldap is set" + when: + - "'ldap' in grafana_auth" + - "grafana_ldap is not defined or ('servers' not in grafana_ldap or 'group_mappings' not in grafana_ldap)" + +- name: "Force grafana_use_provisioning to false if grafana_version is < 5.0 ( grafana_version is set to '{{ grafana_version }}' )" + ansible.builtin.set_fact: + grafana_use_provisioning: false + when: + - "grafana_version != 'latest'" + - "grafana_version is version_compare('5.0', '<')" + +- name: "Fail if grafana_port is lower than 1024 and grafana_cap_net_bind_service is not true" + ansible.builtin.fail: + msg: "Trying to use a port lower than 1024 without setting grafana_cap_net_bind_service." + when: + - "grafana_port | int <= 1024" + - "not grafana_cap_net_bind_service" + +- name: "Fail if grafana_server.socket not defined when in socket mode" + ansible.builtin.fail: + msg: "You need to configure grafana_server.socket when grafana_server.protocol is set to 'socket'" + when: + - "grafana_server.protocol is defined and grafana_server.protocol == 'socket'" + - "grafana_server.socket is undefined or grafana_server.socket == ''" diff --git a/roles/grafana/templates/grafana.ini.j2 b/roles/grafana/templates/grafana.ini.j2 new file mode 100644 index 0000000..4fac6cd --- /dev/null +++ b/roles/grafana/templates/grafana.ini.j2 @@ -0,0 +1,197 @@ +{{ ansible_managed | comment }} +# More informations: +# http://docs.grafana.org/installation/configuration +# https://github.com/grafana/grafana/blob/master/conf/sample.ini + +app_mode = production +instance_name = {{ grafana_instance }} + +# Directories +[paths] +data = {{ grafana_data_dir }} +logs = {{ grafana_logs_dir }} +plugins = {{ grafana_data_dir }}/plugins +; datasources = conf/datasources + +# HTTP options +[server] +{% if grafana_server.protocol is undefined or grafana_server.protocol in ['http', 'https'] %} +http_addr = {{ grafana_address }} +http_port = {{ grafana_port }} +{% endif %} +domain = {{ grafana_domain }} +root_url = {{ grafana_url }} +{% for k,v in grafana_server.items() %} +{% if not k in ['http_addr', 'http_port', 'domain', 'root_url'] %} +{{ k }} = {{ v }} +{% endif %} +{% endfor %} + +# Database +[database] +{% for k,v in grafana_database.items() %} +{% if k == 'password' %} +{{ k }} = """{{ v }}""" +{% else %} +{{ k }} = {{ v }} +{% endif %} +{% endfor %} + +# Remote cache +[remote_cache] +{% for k,v in grafana_remote_cache.items() %} +{{ k }} = {{ v }} +{% endfor %} + +# Security +[security] +{% for k,v in grafana_security.items() %} +{{ k }} = {{ v }} +{% endfor %} + +# Users management and registration +{% if grafana_users != {} %} +[users] +{% for k,v in grafana_users.items() %} +{{ k }} = {{ v }} +{% endfor %} +{% endif %} +[emails] +welcome_email_on_sign_up = {{ grafana_welcome_email_on_sign_up }} + +# Authentication +{% if grafana_auth != {} %} +[auth] +disable_login_form = {{ grafana_auth.disable_login_form | default('False') }} +oauth_auto_login = {{ grafana_auth.oauth_auto_login | default('False') }} +disable_signout_menu = {{ grafana_auth.disable_signout_menu | default('False') }} +signout_redirect_url = {{ grafana_auth.signout_redirect_url | default('') }} +{% for section, options in grafana_auth.items() %} +{% if options is mapping %} +[auth.{{ section }}] +{% if "enabled" not in options %} +enabled = True +{% endif %} +{% for k, v in options.items() %} +{{ k }} = {{ v }} +{% endfor %} +{% else %} +{{ section }} = {{ options }} +{% endif %} +{% endfor %} +{% endif %} + +# Session +{% if grafana_session != {} %} +[session] +{% for k,v in grafana_session.items() %} +{{ k }} = {{ v }} +{% endfor %} +{% endif %} + +# Analytics +[analytics] +reporting_enabled = "{{ grafana_analytics.reporting_enabled | default(True) }}" +{% if grafana_analytics.google_analytics_ua_id is defined and grafana_analytics.google_analytics_ua_id != '' %} +google_analytics_ua_id = "{{ grafana_analytics.google_analytics_ua_id }}" +{% endif %} + +# Dashboards +[dashboards] +versions_to_keep = 20 + +[dashboards.json] +enabled = true +path = {{ grafana_data_dir }}/dashboards + +# Alerting +[alerting] +{% if grafana_alerting != {} %} +enabled = true +{% for k,v in grafana_alerting.items() %} +{% if k != 'enabled' %} +{{ k }} = {{ v }} +{% endif %} +{% endfor %} +{% else %} +enabled = false +{% endif %} + +# SMTP and email config +{% if grafana_smtp != {} %} +[smtp] +enabled = True +{% for k,v in grafana_smtp.items() %} +{% if k == 'enabled' %}{% endif %} +{% if k == 'password' %} +{{ k }} = """{{ v }}""" +{% else %} +{{ k }} = {{ v }} +{% endif %} +{% endfor %} +{% endif %} + +# Logging +[log] +mode = {{ grafana_log.mode | default('console, file') }} +level = {{ grafana_log.level | default('info') }} + +# Metrics +[metrics] +{% if grafana_metrics != {} %} +enabled = true +interval_seconds = {{ grafana_metrics.interval_seconds | default(10) }} +{% if grafana_metrics.basic_auth_username is defined %} +basic_auth_username = {{ grafana_metrics.basic_auth_username }} +{% endif %} +{% if grafana_metrics.basic_auth_password is defined %} +basic_auth_password = """{{ grafana_metrics.basic_auth_password }}""" +{% endif %} +{% if grafana_metrics.graphite is defined %} +[metrics.graphite] +address = {{ grafana_metrics.graphite.address }} +prefix = {{ grafana_metrics.graphite.prefix }} +{% endif %} +{% else %} +enabled = false +{% endif %} + +# Tracing +{% if grafana_tracing != {} %} +[tracing.jaeger] +{% for k,v in grafana_tracing.items() %} +{{ k }} = {{ v }} +{% endfor %} +{% endif %} + +# Grafana.com configuration +[grafana_com] +url = https://grafana.com + +# Snapshots +{% if grafana_snapshots != {} %} +[snapshots] +{% for k,v in grafana_snapshots.items() %} +{{ k }} = {{ v }} +{% endfor %} +{% endif %} + +# External image storage +{% if grafana_image_storage != {} %} +[external_image_storage] +provider = {{ grafana_image_storage.provider }} +[external_image_storage.{{ grafana_image_storage.provider }}] +{% for k,v in grafana_image_storage.items() %} +{% if k != 'provider' %} +{{ k }} = {{ v }} +{% endif %} +{% endfor %} +{% endif %} + +# Panels +{% if grafana_panels != {} %} +[panels] +{% for k,v in grafana_panels.items() %} +{{ k }} = {{ v }} +{% endfor %} +{% endif %} diff --git a/roles/grafana/templates/ldap.toml.j2 b/roles/grafana/templates/ldap.toml.j2 new file mode 100644 index 0000000..b28e002 --- /dev/null +++ b/roles/grafana/templates/ldap.toml.j2 @@ -0,0 +1,39 @@ +{{ ansible_managed | comment }} +# Documentation: http://docs.grafana.org/installation/ldap/ +{% if 'verbose_logging' in grafana_ldap %} +verbose_logging = {{ 'true' if grafana_ldap.verbose_logging else 'false' }} +{% endif %} + +[[servers]] +{% for k,v in grafana_ldap.servers.items() if k != 'attributes' %} +{% if k == 'port' %} +{{ k }} = {{ v | int }} +{% elif v in [True, False] %} +{{ k }} = {{ 'true' if v else 'false' }} +{% else %} +{{ k }} = {{ v | to_nice_json }} +{% endif %} +{% endfor %} + +[servers.attributes] +{% for k,v in grafana_ldap.servers.attributes.items() %} +{{ k }} = {{ v | to_nice_json }} +{% endfor %} + +{% for org in grafana_ldap.group_mappings %} +{% if 'name' in org %} +# {{ org.name }} +{% endif %} +{% for group in org.groups %} +[[servers.group_mappings]] +org_id = {{ org.id }} +{% for k,v in group.items() %} +{% if v in [True, False] %} +{{ k }} = {{ 'true' if v else 'false' }} +{% else %} +{{ k }} = "{{ v }}" +{% endif %} +{% endfor %} + +{% endfor %} +{% endfor %} diff --git a/roles/grafana/templates/tmpfiles.j2 b/roles/grafana/templates/tmpfiles.j2 new file mode 100644 index 0000000..16dda1b --- /dev/null +++ b/roles/grafana/templates/tmpfiles.j2 @@ -0,0 +1,2 @@ +{{ ansible_managed | comment }} +d {{ grafana_server.socket | dirname }} 0775 grafana grafana diff --git a/roles/grafana/test-requirements.txt b/roles/grafana/test-requirements.txt new file mode 100644 index 0000000..af58c11 --- /dev/null +++ b/roles/grafana/test-requirements.txt @@ -0,0 +1,6 @@ +molecule +docker +pytest-testinfra +jmespath +selinux +passlib diff --git a/roles/grafana/vars/distro/debian.yml b/roles/grafana/vars/distro/debian.yml new file mode 100644 index 0000000..ca97730 --- /dev/null +++ b/roles/grafana/vars/distro/debian.yml @@ -0,0 +1,8 @@ +--- +grafana_package: "grafana{% if ansible_architecture == 'armv6l' %}-rpi{% endif %}{{ (grafana_version != 'latest') | ternary('=' ~ grafana_version, '') }}" +_grafana_dependencies: + - apt-transport-https + - adduser + - ca-certificates + - libfontconfig + - gnupg2 diff --git a/roles/grafana/vars/distro/redhat.yml b/roles/grafana/vars/distro/redhat.yml new file mode 100644 index 0000000..0721829 --- /dev/null +++ b/roles/grafana/vars/distro/redhat.yml @@ -0,0 +1,5 @@ +--- +grafana_package: "grafana{{ (grafana_version != 'latest') | ternary('-' ~ grafana_version, '') }}" +# https://unix.stackexchange.com/questions/534463/cant-enable-grafana-on-boot-in-fedora-because-systemd-sysv-install-missing +_grafana_dependencies: + - chkconfig diff --git a/roles/grafana/vars/distro/suse.yml b/roles/grafana/vars/distro/suse.yml new file mode 100644 index 0000000..5dfc0fd --- /dev/null +++ b/roles/grafana/vars/distro/suse.yml @@ -0,0 +1,2 @@ +--- +grafana_package: "grafana{{ (grafana_version != 'latest') | ternary('-' ~ grafana_version, '') }}" diff --git a/roles/grafana_agent/README.md b/roles/grafana_agent/README.md index 993e37d..d6003fb 100644 --- a/roles/grafana_agent/README.md +++ b/roles/grafana_agent/README.md @@ -26,7 +26,7 @@ All variables which can be overridden are stored in [./defaults/main.yaml](./def | `grafana_agent_mode` | `static` | mode to run Grafana Agent in. Can be "flow" or "static", [Flow Docs](https://grafana.com/docs/agent/latest/flow/) | | `grafana_agent_user` | `grafana-agent` | os user to create for the agent to run as | | `grafana_agent_user_group` | `grafana-agent` | os user group to create for the agent | -| `grafana_agent_user_groups` | `[]` | Configurable user groups that the grafana agent can be put in so that it can access logs | +| `grafana_agent_user_groups` | `[]` | Configurable user groups that the Grafana agent can be put in so that it can access logs | | `grafana_agent_user_shell` | `/usr/sbin/nologin` | the shell for the user | | `grafana_agent_user_createhome` | `false` | whether or not to create a home directory for the user | | `grafana_agent_local_binary_file` | `""` | full path to the local binary if already downloaded or built on the controller, this should only be set, if ansible is not downloading the binary and you have manually downloaded the binary | diff --git a/tests/integration/targets/molecule-grafana-alternative/runme.sh b/tests/integration/targets/molecule-grafana-alternative/runme.sh new file mode 100755 index 0000000..87129f7 --- /dev/null +++ b/tests/integration/targets/molecule-grafana-alternative/runme.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +version="0.1.3" +src="https://github.com/gardar/ansible-test-molecule/releases/download/$version/ansible-test-molecule.sh" + +# shellcheck disable=SC1090 +if [[ -v GITHUB_TOKEN ]] +then + source <(curl -L -s -H "Authorization: token $GITHUB_TOKEN" $src) +else + source <(curl -L -s $src) +fi diff --git a/tests/integration/targets/molecule-grafana-default/runme.sh b/tests/integration/targets/molecule-grafana-default/runme.sh new file mode 100755 index 0000000..87129f7 --- /dev/null +++ b/tests/integration/targets/molecule-grafana-default/runme.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +version="0.1.3" +src="https://github.com/gardar/ansible-test-molecule/releases/download/$version/ansible-test-molecule.sh" + +# shellcheck disable=SC1090 +if [[ -v GITHUB_TOKEN ]] +then + source <(curl -L -s -H "Authorization: token $GITHUB_TOKEN" $src) +else + source <(curl -L -s $src) +fi