diff --git a/.config/ansible-lint.yml b/.config/ansible-lint.yml new file mode 100644 index 00000000..81424ef7 --- /dev/null +++ b/.config/ansible-lint.yml @@ -0,0 +1,5 @@ +--- +exclude_paths: + - .github/ + - tests/ + - changelogs/ diff --git a/.config/yamllint/config b/.config/yamllint/config new file mode 100644 index 00000000..e907140b --- /dev/null +++ b/.config/yamllint/config @@ -0,0 +1,2 @@ +--- +extends: default diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index 85fbc432..a5717781 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: python_version: ["3.10"] - ansible_version: ["stable-2.13", "stable-2.14", "devel"] + ansible_version: ["stable-2.15", "stable-2.16", "devel"] steps: - name: Perform testing uses: ansible-community/ansible-test-gh-action@release/v1 @@ -30,7 +30,7 @@ jobs: strategy: matrix: python_version: ["3.10"] - ansible_version: ["stable-2.13", "stable-2.14", "devel"] + ansible_version: ["stable-2.15", "stable-2.16", "devel"] steps: - name: Perform testing uses: ansible-community/ansible-test-gh-action@release/v1 @@ -46,7 +46,7 @@ jobs: fail-fast: false matrix: grafana_version: ["9.5.14", "8.5.27", "10.2.2"] - ansible_version: ["stable-2.13", "stable-2.14", "devel"] + ansible_version: ["stable-2.15", "stable-2.16", "devel"] python_version: ["3.10"] services: grafana: @@ -58,3 +58,37 @@ jobs: ansible-core-version: ${{ matrix.ansible_version }} target-python-version: ${{ matrix.python_version }} testing-type: integration + + molecule: + runs-on: ubuntu-latest + env: + PY_COLORS: 1 + ANSIBLE_FORCE_COLOR: 1 + strategy: + fail-fast: false + matrix: + grafana_version: ["9.5.14", "8.5.27", "10.2.2"] + ansible_version: ["stable-2.15", "stable-2.16", "devel"] + python_version: ["3.10"] + services: + grafana: + image: grafana/grafana:${{ matrix.grafana_version }} + ports: ["3000:3000"] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python_version }} + + - name: Install dependencies + run: | + python -m pip install --no-cache-dir --upgrade pip + pip install "git+https://github.com/ansible/ansible@${{ matrix.ansible_version }}" molecule molecule-docker + + - name: Test with molecule + run: | + molecule --version + molecule test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fefe85bf..26216d89 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -27,3 +27,10 @@ jobs: uses: chartboost/ruff-action@v1 with: src: ${{ steps.changed-files.outputs.all_changed_files }} + ansible: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run ansible-lint + uses: ansible/ansible-lint@main diff --git a/README.md b/README.md index 1780b2fd..6d23f1b9 100644 --- a/README.md +++ b/README.md @@ -121,45 +121,6 @@ In your playbooks, you can set [module defaults](https://github.com/ansible/ansi is_admin: true ``` -## Complementary Collection: [`telekom-mms.grafana`](https://github.com/telekom-mms/ansible-role-grafana) - -The `telekom-mms.grafana` collection is an Ansible Collection that simplifies the use of the `community.grafana` collection. It provides an Ansible Role for easy integration with `community.grafana`. With this collection, you only need to define the variables for your Grafana resources. - -### Requirements - ansible-galaxy collection install telekom-mms.grafana -... or use a requirements.yml: -`ansible-galaxy collection install -r requirements.yml` -```yaml ---- -collections: - - name: telekom-mms.grafana -``` - -### Example Playbook -```yaml ---- -- hosts: localhost - gather_facts: false - connection: local - - vars: - grafana_url: "https://grafana.company.com" - grafana_user: "admin" - grafana_password: "xxxxxx" - - grafana_datasources: - - name: "Loki" - ds_type: "loki" - ds_url: "http://127.0.0.1:3100" - tls_skip_verify: yes - grafana_folders: - - name: my_service - - name: other_service - - roles: - - role: telekom-mms.grafana -``` - ## Testing and Development If you want to develop new content for this collection or improve what's already here, the easiest way to work on the collection is to clone it into one of the configured [`COLLECTIONS_PATHS`](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths), and work on it there. diff --git a/changelogs/fragments/343-telekom-mms-role.yml b/changelogs/fragments/343-telekom-mms-role.yml new file mode 100644 index 00000000..07da942c --- /dev/null +++ b/changelogs/fragments/343-telekom-mms-role.yml @@ -0,0 +1,2 @@ +minor_changes: + - Merged ansible role telekom-mms/ansible-role-grafana into ansible-collections/community.grafana diff --git a/codecov.yml b/codecov.yml index c01a21d4..d376ab57 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,4 +3,4 @@ coverage: round: down range: "70...100" fixes: - - "/ansible_collections/community/grafana/::" + - "/ansible_collections/community/grafana/::" diff --git a/meta/runtime.yml b/meta/runtime.yml index 06ab11f5..15f5554f 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,5 +1,5 @@ --- -requires_ansible: ">=2.9.0" +requires_ansible: ">=2.14.0" action_groups: grafana: - grafana_dashboard diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 00000000..b1e1cf20 --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,48 @@ +--- +- name: Converge + hosts: localhost + environment: + http_proxy: "{{ lookup('env', 'HTTP_PROXY') | default(omit) }}" + https_proxy: "{{ lookup('env', 'HTTPS_PROXY') | default(omit) }}" + no_proxy: "{{ lookup('env', 'NO_PROXY') | default(omit) }}" + + vars: + grafana_url: http://localhost:3000 + grafana_username: admin + grafana_password: admin + + grafana_organizations: + - name: my_org + + grafana_datasources: + - name: Loki + ds_type: loki + ds_url: http://127.0.0.1:3100 + tls_skip_verify: true + + grafana_folders: + - name: my_service + - name: other_service + + grafana_teams: + - name: my_team + email: myteam@example.de + + grafana_users: + - name: Test User + login: testuser + password: supersecure!123 + email: testuser@example.de + + grafana_organization_users: + - login: testuser + org_id: 1 + - login: testuser + org_name: my_org + + grafana_dashboards: + - folder: my_service + path: test_dashboard.json + overwrite: true + + roles: [{role: community.grafana.grafana}] diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 00000000..ab4613bc --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,21 @@ +--- +dependency: + name: galaxy +driver: + name: docker +platforms: + - name: instance + image: rndmh3ro/docker-debian12-ansible:latest + command: ${MOLECULE_DOCKER_COMMAND:-/lib/systemd/systemd} + env: + container: docker + pre_build_image: true + platform: amd64 +provisioner: + name: ansible + config_options: + defaults: + interpreter_python: auto_silent + callback_whitelist: profile_tasks, timer, yaml +verifier: + name: ansible diff --git a/molecule/default/test_dashboard.json b/molecule/default/test_dashboard.json new file mode 100644 index 00000000..b9518451 --- /dev/null +++ b/molecule/default/test_dashboard.json @@ -0,0 +1,126 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "loki" + }, + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 33, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "New dashboard", + "uid": "ES5apb27k", + "version": 1, + "weekStart": "" +} diff --git a/plugins/modules/grafana_dashboard.py b/plugins/modules/grafana_dashboard.py index 18ab041b..4fa1c6c9 100644 --- a/plugins/modules/grafana_dashboard.py +++ b/plugins/modules/grafana_dashboard.py @@ -90,42 +90,39 @@ """ EXAMPLES = """ -- hosts: localhost - connection: local - tasks: - - name: Import Grafana dashboard foo - community.grafana.grafana_dashboard: - grafana_url: http://grafana.company.com - grafana_api_key: "{{ grafana_api_key }}" - state: present - commit_message: Updated by ansible - overwrite: true - path: /path/to/dashboards/foo.json - - - name: Import Grafana dashboard Zabbix - community.grafana.grafana_dashboard: - grafana_url: http://grafana.company.com - grafana_api_key: "{{ grafana_api_key }}" - folder: zabbix - dashboard_id: 6098 - dashboard_revision: 1 - - - name: Import Grafana dashboard zabbix - community.grafana.grafana_dashboard: - grafana_url: http://grafana.company.com - grafana_api_key: "{{ grafana_api_key }}" - folder: public - dashboard_url: https://grafana.com/api/dashboards/6098/revisions/1/download - - - name: Export dashboard - community.grafana.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" +- name: Import Grafana dashboard foo + community.grafana.grafana_dashboard: + grafana_url: http://grafana.company.com + grafana_api_key: "{{ grafana_api_key }}" + state: present + commit_message: Updated by ansible + overwrite: true + path: /path/to/dashboards/foo.json + +- name: Import Grafana dashboard Zabbix + community.grafana.grafana_dashboard: + grafana_url: http://grafana.company.com + grafana_api_key: "{{ grafana_api_key }}" + folder: zabbix + dashboard_id: 6098 + dashboard_revision: 1 + +- name: Import Grafana dashboard zabbix + community.grafana.grafana_dashboard: + grafana_url: http://grafana.company.com + grafana_api_key: "{{ grafana_api_key }}" + folder: public + dashboard_url: https://grafana.com/api/dashboards/6098/revisions/1/download + +- name: Export dashboard + community.grafana.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 = """ diff --git a/roles/grafana/README.md b/roles/grafana/README.md new file mode 100644 index 00000000..2062241c --- /dev/null +++ b/roles/grafana/README.md @@ -0,0 +1,120 @@ +# Grafana Role for Ansible Collection Community.Grafana + +Configure Grafana organizations, dashboards, folders, datasources, teams and users. + +## Role Variables + +| Variable | Required | Default | +| ---------------- | -------- | ------- | +| grafana_url | yes | +| grafana_username | yes | +| grafana_password | yes | +| [**grafana_users**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_user_module.html) | +| email | no | +| is_admin | no | +| login | yes | +| name | yes | +| password | no | +| state | no | +| [**grafana_organizations**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_organization_module.html) | +| name | yes | +| state | no | +| [**grafana_teams**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_team_module.html) | +| email | yes | +| enforce_members | no | +| members | no | +| name | yes | +| skip_version_check | no | +| state | no | +| [**grafana_datasources**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_datasource_module.html) | +| access | no | +| additional_json_data | no | +| additional_secure_json_data | no | +| aws_access_key | no | +| aws_assume_role_arn | no | +| aws_auth_type | no | +| aws_credentials_profile | no | +| aws_custom_metrics_namespaces | no | +| aws_default_region | no | +| aws_secret_key | no | +| azure_client | no | +| azure_cloud | no | +| azure_secret | no | +| azure_tenant | no | +| basic_auth_password | no | +| basic_auth_user | no | +| database | no | +| ds_type | no | +| ds_url | no | +| enforce_secure_data | no | +| es_version | no | +| interval | no | +| is_default | no | +| max_concurrent_shard_requests | no | +| name | yes | +| org_id | no | +| org_name | no | +| password | no | +| sslmode | no | +| state | no | +| time_field | no | +| time_interval | no | +| tls_ca_cert | no | +| tls_client_cert | no | +| tls_client_key | no | +| tls_skip_verify | no | +| trends | no | +| tsdb_resolution | no | +| tsdb_version | no | +| uid | no | +| user | no | +| with_credentials | no | +| zabbix_password | no | +| zabbix_user | no | +| [**grafana_folders**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_folder_module.html) | +| name | yes | +| skip_version_check | no | +| state | no | +| [**grafana_dashboards**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_dashboard_module.html) | +| commit_message | no | +| dashboard_id | no | +| dashboard_revision | no | +| folder | no | +| org_id | no | +| org_name | no | +| overwrite | no | +| path | no | +| slug | no | +| state | no | +| uid | no | +| [**grafana_organization_users**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_organization_user_module.html) | +| login | yes | +| org_id | no | +| org_name | no | +| role | no | +| state | no | + +## Example Playbook + +```yaml +--- +- hosts: localhost + gather_facts: false + + vars: + grafana_url: "https://monitoring.example.com" + grafana_username: "api-user" + grafana_password: "******" + + grafana_datasources: + - name: "Loki" + ds_type: "loki" + ds_url: "http://127.0.0.1:3100" + tls_skip_verify: yes + grafana_folders: + - name: my_service + - name: other_service + + roles: + - role: community.grafana.grafana +``` diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml new file mode 100644 index 00000000..d23719c5 --- /dev/null +++ b/roles/grafana/defaults/main.yml @@ -0,0 +1,8 @@ +--- +grafana_organizations: [] +grafana_organization_users: [] +grafana_users: [] +grafana_teams: [] +grafana_datasources: [] +grafana_folders: [] +grafana_dashboards: [] diff --git a/roles/grafana/meta/main.yml b/roles/grafana/meta/main.yml new file mode 100644 index 00000000..47d4af5b --- /dev/null +++ b/roles/grafana/meta/main.yml @@ -0,0 +1,14 @@ +--- +galaxy_info: + role_name: grafana + author: community + description: Configure Grafana organizations, dashboards, folders, datasources, teams and users + license: GPLv3 + min_ansible_version: "2.14" + galaxy_tags: [grafana, monitoring] + platforms: + - {name: EL, versions: [all]} + - {name: Fedora, versions: [all]} + - {name: Amazon, versions: [all]} + - {name: Debian, versions: [all]} + - {name: Ubuntu, versions: [all]} diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml new file mode 100644 index 00000000..afe8adfc --- /dev/null +++ b/roles/grafana/tasks/main.yml @@ -0,0 +1,128 @@ +--- +- name: Group tasks for authentication parameters + module_defaults: + group/community.grafana.grafana: + url: "{{ grafana_url }}" + url_username: "{{ grafana_username }}" + url_password: "{{ grafana_password }}" + use_proxy: "{{ grafana_use_proxy | default(omit) }}" + validate_certs: "{{ grafana_validate_certs | default(omit) }}" + block: + - name: Manage organization # noqa: args[module] + community.grafana.grafana_organization: + name: "{{ organization.name }}" + state: "{{ organization.state | default(omit) }}" + loop: "{{ grafana_organizations }}" + loop_control: {loop_var: organization} + tags: organization + + - name: Manage datasource + community.grafana.grafana_datasource: + access: "{{ datasource.access | default(omit) }}" + additional_json_data: "{{ datasource.additional_json_data | default(omit) }}" + additional_secure_json_data: "{{ datasource.additional_secure_json_data | default(omit) }}" + aws_access_key: "{{ datasource.aws_access_key | default(omit) }}" + aws_assume_role_arn: "{{ datasource.aws_assume_role_arn | default(omit) }}" + aws_auth_type: "{{ datasource.aws_auth_type | default(omit) }}" + aws_credentials_profile: "{{ datasource.aws_credentials_profile | default(omit) }}" + aws_custom_metrics_namespaces: "{{ datasource.aws_custom_metrics_namespaces | default(omit) }}" + aws_default_region: "{{ datasource.aws_default_region | default(omit) }}" + aws_secret_key: "{{ datasource.aws_secret_key | default(omit) }}" + azure_client: "{{ datasource.azure_client | default(omit) }}" + azure_cloud: "{{ datasource.azure_cloud | default(omit) }}" + azure_secret: "{{ datasource.azure_secret | default(omit) }}" + azure_tenant: "{{ datasource.azure_tenant | default(omit) }}" + basic_auth_password: "{{ datasource.basic_auth_password | default(omit) }}" + basic_auth_user: "{{ datasource.basic_auth_user | default(omit) }}" + database: "{{ datasource.database | default(omit) }}" + ds_type: "{{ datasource.ds_type | default(omit) }}" + ds_url: "{{ datasource.ds_url | default(omit) }}" + enforce_secure_data: "{{ datasource.enforce_secure_data | default(omit) }}" + es_version: "{{ datasource.es_version | default(omit) }}" + interval: "{{ datasource.interval | default(omit) }}" + is_default: "{{ datasource.is_default | default(omit) }}" + max_concurrent_shard_requests: "{{ datasource.max_concurrent_shard_requests | default(omit) }}" + name: "{{ datasource.name }}" + org_id: "{{ datasource.org_id | default(omit) }}" + org_name: "{{ datasource.org_name | default(omit) }}" + password: "{{ datasource.password | default(omit) }}" + sslmode: "{{ datasource.sslmode | default(omit) }}" + state: "{{ datasource.state | default(omit) }}" + time_field: "{{ datasource.time_field | default(omit) }}" + time_interval: "{{ datasource.time_interval | default(omit) }}" + tls_ca_cert: "{{ datasource.tls_ca_cert | default(omit) }}" + tls_client_cert: "{{ datasource.tls_client_cert | default(omit) }}" + tls_client_key: "{{ datasource.tls_client_key | default(omit) }}" + tls_skip_verify: "{{ datasource.tls_skip_verify | default(omit) }}" + trends: "{{ datasource.trends | default(omit) }}" + tsdb_resolution: "{{ datasource.tsdb_resolution | default(omit) }}" + tsdb_version: "{{ datasource.tsdb_version | default(omit) }}" + uid: "{{ datasource.uid | default(omit) }}" + user: "{{ datasource.user | default(omit) }}" + with_credentials: "{{ datasource.with_credentials | default(omit) }}" + zabbix_password: "{{ datasource.zabbix_password | default(omit) }}" + zabbix_user: "{{ datasource.zabbix_user | default(omit) }}" + loop: "{{ grafana_datasources }}" + loop_control: {loop_var: datasource} + tags: datasource + + - name: Manage folder # noqa: args[module] + community.grafana.grafana_folder: + name: "{{ folder.name }}" + skip_version_check: "{{ folder.skip_version_check | default(omit) }}" + state: "{{ folder.state | default(omit) }}" + loop: "{{ grafana_folders }}" + loop_control: {loop_var: folder} + tags: folder + + - name: Manage team # noqa: args[module] + community.grafana.grafana_team: + email: "{{ team.email }}" + enforce_members: "{{ team.enforce_members | default(omit) }}" + members: "{{ team.members | default(omit) }}" + name: "{{ team.name }}" + skip_version_check: "{{ team.skip_version_check | default(omit) }}" + state: "{{ team.state | default(omit) }}" + loop: "{{ grafana_teams }}" + loop_control: {loop_var: team} + tags: team + + - name: Manage user # noqa: args[module] + community.grafana.grafana_user: + email: "{{ user.email | default(omit) }}" + is_admin: "{{ user.is_admin | default(omit) }}" + login: "{{ user.login }}" + name: "{{ user.name }}" + password: "{{ user.password | default(omit) }}" + state: "{{ user.state | default(omit) }}" + loop: "{{ grafana_users }}" + loop_control: {loop_var: user} + tags: user + + - name: Manage organization users + community.grafana.grafana_organization_user: + login: "{{ organization_user.login }}" + org_id: "{{ organization_user.org_id | default(omit) }}" + org_name: "{{ organization_user.org_name | default(omit) }}" + role: "{{ organization_user.role | default(omit) }}" + state: "{{ organization_user.state | default(omit) }}" + loop: "{{ grafana_organization_users }}" + loop_control: {loop_var: organization_user} + tags: organization_user + + - name: Manage dashboard + community.grafana.grafana_dashboard: + commit_message: "{{ dashboard.commit_message | default(omit) }}" + dashboard_id: "{{ dashboard.dashboard_id | default(omit) }}" + dashboard_revision: "{{ dashboard.dashboard_revision | default(omit) }}" + folder: "{{ dashboard.folder | default(omit) }}" + org_id: "{{ dashboard.org_id | default(omit) }}" + org_name: "{{ dashboard.org_name | default(omit) }}" + overwrite: "{{ dashboard.overwrite | default(omit) }}" + path: "{{ dashboard.path | default(omit) }}" + slug: "{{ dashboard.slug | default(omit) }}" + state: "{{ dashboard.state | default(omit) }}" + uid: "{{ dashboard.uid | default(omit) }}" + loop: "{{ grafana_dashboards }}" + loop_control: {loop_var: dashboard} + tags: [dashboard, molecule-idempotence-notest]