Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(grafana_folder): manage folders for organizations #347

Merged
merged 11 commits into from
Feb 7, 2024
7 changes: 7 additions & 0 deletions changelogs/fragments/347-folder-for-orgs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---

minor_changes:
- Manage `grafana_folder` for organizations

trivial:
- Fixed syntax for code in some docs
4 changes: 2 additions & 2 deletions plugins/modules/grafana_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
description:
- The Grafana organization ID where the dashboard will be imported / exported / deleted.
- Not used when I(grafana_api_key) is set, because the grafana_api_key only belongs to one organization.
- Mutually exclusive with `org_name`.
- Mutually exclusive with C(org_name).
default: 1
type: int
org_name:
description:
- The Grafana organization name where the dashboard will be imported / exported / deleted.
- Not used when I(grafana_api_key) is set, because the grafana_api_key only belongs to one organization.
- Mutually exclusive with `org_id`.
- Mutually exclusive with C(org_id).
type: str
folder:
description:
Expand Down
4 changes: 2 additions & 2 deletions plugins/modules/grafana_datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@
- Grafana organization ID in which the datasource should be created.
- Not used when C(grafana_api_key) is set, because the C(grafana_api_key) only
belongs to one organization.
- Mutually exclusive with `org_name`.
- Mutually exclusive with C(org_name).
default: 1
type: int
org_name:
description:
- Grafana organization name in which the datasource should be created.
- Not used when C(grafana_api_key) is set, because the C(grafana_api_key) only
belongs to one organization.
- Mutually exclusive with `org_id`.
- Mutually exclusive with C(org_id).
type: str
state:
description:
Expand Down
73 changes: 55 additions & 18 deletions plugins/modules/grafana_folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,25 @@
default: present
type: str
choices: ["present", "absent"]
org_id:
description:
- Grafana organization ID in which the datasource should be created.
- Not used when C(grafana_api_key) is set, because the C(grafana_api_key) only
belongs to one organization.
- Mutually exclusive with C(org_name).
default: 1
type: int
org_name:
description:
- Grafana organization name in which the datasource should be created.
- Not used when C(grafana_api_key) is set, because the C(grafana_api_key) only
belongs to one organization.
- Mutually exclusive with C(org_id).
type: str
skip_version_check:
description:
- Skip Grafana version check and try to reach api endpoint anyway.
- This parameter can be useful if you enabled `hide_version` in grafana.ini
- This parameter can be useful if you enabled C(hide_version) in grafana.ini
required: False
type: bool
default: false
Expand Down Expand Up @@ -179,6 +194,8 @@
class GrafanaFolderInterface(object):
def __init__(self, module):
self._module = module
self.grafana_url = base.clean_url(module.params.get("url"))
self.org_id = None
# {{{ Authentication header
self.headers = {"Content-Type": "application/json"}
if module.params.get("grafana_api_key", None):
Expand All @@ -189,8 +206,13 @@
self.headers["Authorization"] = basic_auth_header(
module.params["url_username"], module.params["url_password"]
)
self.org_id = (
self.organization_by_name(module.params["org_name"])
if module.params["org_name"]
else module.params["org_id"]
)
self.switch_organization(self.org_id)
# }}}
self.grafana_url = base.clean_url(module.params.get("url"))
if module.params.get("skip_version_check") is False:
try:
grafana_version = self.get_version()
Expand Down Expand Up @@ -233,6 +255,21 @@
failed=True, msg="Grafana Folders API answered with HTTP %d" % status_code
)

def switch_organization(self, org_id):
url = "/api/user/using/%d" % org_id
self._send_request(url, headers=self.headers, method="POST")

def organization_by_name(self, org_name):
url = "/api/user/orgs"
organizations = self._send_request(url, headers=self.headers, method="GET")
orga = next((org for org in organizations if org["name"] == org_name))
if orga:
return orga["orgId"]

self._module.fail_json(

Check warning on line 269 in plugins/modules/grafana_folder.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/grafana_folder.py#L269

Added line #L269 was not covered by tests
failed=True, msg="Current user isn't member of organization: %s" % org_name
)

def get_version(self):
url = "/api/health"
response = self._send_request(
Expand Down Expand Up @@ -266,28 +303,28 @@
return response


def setup_module_object():
def main():
argument_spec = base.grafana_argument_spec()
argument_spec.update(
name=dict(type="str", aliases=["title"], required=True),
state=dict(type="str", default="present", choices=["present", "absent"]),
skip_version_check=dict(type="bool", default=False),
org_id=dict(default=1, type="int"),
org_name=dict(type="str"),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=base.grafana_required_together(),
mutually_exclusive=base.grafana_mutually_exclusive(),
required_together=base.grafana_required_together()
+ [["url_username", "url_password", "org_id"]],
mutually_exclusive=base.grafana_mutually_exclusive()
+ [
["org_id", "org_name"],
],
)
return module


argument_spec = base.grafana_argument_spec()
argument_spec.update(
name=dict(type="str", aliases=["title"], required=True),
state=dict(type="str", default="present", choices=["present", "absent"]),
skip_version_check=dict(type="bool", default=False),
)


def main():
module = setup_module_object()
state = module.params["state"]
title = module.params["name"]
module.params["url"] = base.clean_url(module.params["url"])

grafana_iface = GrafanaFolderInterface(module)

Expand Down
4 changes: 2 additions & 2 deletions plugins/modules/grafana_organization_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@
default: 1
description:
- Organization ID.
- Mutually exclusive with `org_name`.
- Mutually exclusive with C(org_name).
org_name:
type: str
description:
- Organization name.
- Mutually exclusive with `org_id`.
- Mutually exclusive with C(org_id).

extends_documentation_fragment:
- community.grafana.basic_auth
Expand Down
2 changes: 1 addition & 1 deletion plugins/modules/grafana_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
skip_version_check:
description:
- Skip Grafana version check and try to reach api endpoint anyway.
- This parameter can be useful if you enabled `hide_version` in grafana.ini
- This parameter can be useful if you enabled C(hide_version) in grafana.ini
required: False
type: bool
default: false
Expand Down
2 changes: 1 addition & 1 deletion plugins/modules/grafana_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
type: str
choices: ["present", "absent"]
notes:
- Unlike other modules from the collection, this module does not support `grafana_api_key` authentication type. The Grafana API endpoint for users management
- Unlike other modules from the collection, this module does not support C(grafana_api_key) authentication type. The Grafana API endpoint for users management
requires basic auth and admin privileges.
extends_documentation_fragment:
- community.grafana.basic_auth
Expand Down
2 changes: 2 additions & 0 deletions roles/grafana/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Configure Grafana organizations, dashboards, folders, datasources, teams and use
| name | yes |
| skip_version_check | no |
| state | no |
| org_id | no |
| org_name | no |
| [**grafana_dashboards**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_dashboard_module.html) |
| commit_message | no |
| dashboard_id | no |
Expand Down
2 changes: 2 additions & 0 deletions roles/grafana/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
name: "{{ folder.name }}"
skip_version_check: "{{ folder.skip_version_check | default(omit) }}"
state: "{{ folder.state | default(omit) }}"
org_id: "{{ folder.org_id | default(omit) }}"
org_name: "{{ folder.org_name | default(omit) }}"
loop: "{{ grafana_folders }}"
loop_control: {loop_var: folder}
tags: folder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
- name: Create a Folder
community.grafana.grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: present
register: result

- ansible.builtin.assert:
that:
- result.changed == true
- result.folder.title == 'grafana_working_group'
when: not ansible_check_mode

- name: Test folder creation idempotency
community.grafana.grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: present
register: result

- ansible.builtin.assert:
that:
- result.changed == false
- result.folder.title == 'grafana_working_group'
when: not ansible_check_mode

- name: Delete a Folder
community.grafana.grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: absent
register: result

- ansible.builtin.assert:
that:
- result.changed == true
when: not ansible_check_mode

- name: Test folder deletion idempotency
community.grafana.grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: absent
register: result

- ansible.builtin.assert:
that:
- result.changed == false
when: not ansible_check_mode
60 changes: 4 additions & 56 deletions tests/integration/targets/grafana_folder/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -1,58 +1,6 @@
---
- name: Create a Folder
grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: present
register: result
- name: Folder creation and deletion
ansible.builtin.include_tasks: create-delete.yml

- ansible.builtin.assert:
that:
- result.changed == true
- result.folder.title == 'grafana_working_group'
when: not ansible_check_mode

- name: Test folder creation idempotency
grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: present
register: result

- ansible.builtin.assert:
that:
- result.changed == false
- result.folder.title == 'grafana_working_group'
when: not ansible_check_mode

- name: Delete a Folder
grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: absent
register: result

- ansible.builtin.assert:
that:
- result.changed == true
when: not ansible_check_mode

- name: Test folder deletion idempotency
grafana_folder:
url: "{{ grafana_url }}"
url_username: "{{ grafana_username }}"
url_password: "{{ grafana_password }}"
title: grafana_working_group
state: absent
register: result

- ansible.builtin.assert:
that:
- result.changed == false
when: not ansible_check_mode
- name: Folder creation and deletion for organization
ansible.builtin.include_tasks: org.yml
7 changes: 7 additions & 0 deletions tests/integration/targets/grafana_folder/tasks/org.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
- module_defaults:
community.grafana.grafana_folder:
org_name: Main Org.
block:
- name: Folder creation and deletion
ansible.builtin.include_tasks: create-delete.yml
Loading