From 7547432236c77088fb04db1d24e1dab973e88483 Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 09:47:23 +0200 Subject: [PATCH 01/10] docs: role readme org_id and org_name --- roles/grafana/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/grafana/README.md b/roles/grafana/README.md index f76e687f..b26a9044 100644 --- a/roles/grafana/README.md +++ b/roles/grafana/README.md @@ -168,6 +168,8 @@ Configure Grafana organizations, dashboards, folders, datasources, teams and use | created_by | yes | | ends_at | yes | | matchers | yes | +| org_id | no | +| org_name | no | | starts_at | yes | | state | no | From 8ce66da0e0d5e455bdeac5428df9e3c804ab1fda Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 09:48:06 +0200 Subject: [PATCH 02/10] feat: role task org_id and org_name --- roles/grafana/tasks/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml index 1665a856..f5e41021 100644 --- a/roles/grafana/tasks/main.yml +++ b/roles/grafana/tasks/main.yml @@ -206,9 +206,11 @@ community.grafana.grafana_silence: comment: "{{ silence.comment }}" created_by: "{{ silence.created_by }}" - starts_at: "{{ silence.starts_at }}" ends_at: "{{ silence.ends_at }}" matchers: "{{ silence.matchers }}" + org_id: "{{ datasource.org_id | default(omit) }}" + org_name: "{{ datasource.org_name | default(omit) }}" + starts_at: "{{ silence.starts_at }}" state: "{{ silence.state | default(omit) }}" loop: "{{ grafana_silences }}" loop_control: {loop_var: silence} From 9d88bb0df548d1843670fe59651e578bdf57ccea Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 09:48:41 +0200 Subject: [PATCH 03/10] docs: module args org_id and org_name --- plugins/modules/grafana_silence.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 35f54a3d..2b2897cb 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -30,6 +30,18 @@ requirements: - The Alertmanager API is only available starting Grafana 8 and the module will fail if the server version is lower than version 8. options: + org_id: + description: + - The Grafana organization ID where the silence will be created or deleted. + - Not used when I(grafana_api_key) is set, because the grafana_api_key only belongs to one organization. + - Mutually exclusive with C(org_name). + default: 1 + type: int + org_name: + description: + - The Grafana organization name where the silence will be created or deleted. + - Not used when I(grafana_api_key) is set, because the grafana_api_key only belongs to one organization. + - Mutually exclusive with C(org_id). comment: description: - The comment that describes the silence. From 213f3fd6343db811b449a2c8092ca48d4a6bae9f Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 09:49:56 +0200 Subject: [PATCH 04/10] feat: class init switch org --- plugins/modules/grafana_silence.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 2b2897cb..19932be9 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -193,9 +193,10 @@ class GrafanaError(Exception): class GrafanaSilenceInterface(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"} - module.params["force_basic_auth"] = True if module.params.get("grafana_api_key", None): self.headers["Authorization"] = ( "Bearer %s" % module.params["grafana_api_key"] @@ -204,8 +205,14 @@ def __init__(self, module): 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() From d59bec9153b526dea83a492804ef70e1f83b9689 Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 09:50:27 +0200 Subject: [PATCH 05/10] feat: switch org and get org functions --- plugins/modules/grafana_silence.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 19932be9..fd8d1701 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -252,6 +252,21 @@ def _send_request(self, url, data=None, headers=None, method="GET"): failed=True, msg="Grafana Silences 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"] + + return self._module.fail_json( + failed=True, msg="Current user isn't member of organization: %s" % org_name + ) + def get_version(self): url = "/api/health" response = self._send_request( From 0eb1ea58a18351c1654b6636823a14654ca02da8 Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 09:50:46 +0200 Subject: [PATCH 06/10] feat: module args --- plugins/modules/grafana_silence.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index fd8d1701..53dea9cc 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -344,23 +344,24 @@ def setup_module_object(): argument_spec = base.grafana_argument_spec() argument_spec.update( comment=dict(type="str", required=True), - state=dict(type="str", choices=["present", "absent"], default="present"), created_by=dict(type="str", required=True), - starts_at=dict(type="str", required=True), ends_at=dict(type="str", required=True), matchers=dict(type="list", elements="dict", required=True), + org_id=dict(default=1, type="int"), + org_name=dict(type="str"), skip_version_check=dict(type="bool", default=False), + starts_at=dict(type="str", required=True), + state=dict(type="str", choices=["present", "absent"], default="present"), ) def main(): - module = setup_module_object() comment = module.params["comment"] created_by = module.params["created_by"] - starts_at = module.params["starts_at"] ends_at = module.params["ends_at"] matchers = module.params["matchers"] + starts_at = module.params["starts_at"] state = module.params["state"] changed = False @@ -372,7 +373,6 @@ def main(): ) if state == "present": - if not silence: silence = grafana_iface.create_silence( comment, created_by, starts_at, ends_at, matchers @@ -396,6 +396,7 @@ def main(): changed=changed, msg="Silence does not exist", ) + module.exit_json(failed=failed, changed=changed, silence=silence) From f5d059313672c3abab68e83f6119fdf5f769db7e Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 09:53:41 +0200 Subject: [PATCH 07/10] docs: changelog fragment --- changelogs/fragments/371-silence-org-switch.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/371-silence-org-switch.yml diff --git a/changelogs/fragments/371-silence-org-switch.yml b/changelogs/fragments/371-silence-org-switch.yml new file mode 100644 index 00000000..140237ad --- /dev/null +++ b/changelogs/fragments/371-silence-org-switch.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - add org switch by `org_id` and `org_name` in `grafana_silence` From c1946f9076b8a9be38f4d6c537accb96fd2344e4 Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 10:03:04 +0200 Subject: [PATCH 08/10] test: recode silence tests and add org tests --- .../grafana_silence/tasks/create-delete.yml | 83 +++++++++++++++++ .../targets/grafana_silence/tasks/main.yml | 91 +------------------ .../targets/grafana_silence/tasks/org.yml | 7 ++ 3 files changed, 94 insertions(+), 87 deletions(-) create mode 100644 tests/integration/targets/grafana_silence/tasks/create-delete.yml create mode 100644 tests/integration/targets/grafana_silence/tasks/org.yml diff --git a/tests/integration/targets/grafana_silence/tasks/create-delete.yml b/tests/integration/targets/grafana_silence/tasks/create-delete.yml new file mode 100644 index 00000000..1561eb59 --- /dev/null +++ b/tests/integration/targets/grafana_silence/tasks/create-delete.yml @@ -0,0 +1,83 @@ +--- +- module_defaults: + community.grafana.grafana_silence: + url: "{{ grafana_url }}" + url_username: "{{ grafana_username }}" + url_password: "{{ grafana_password }}" + block: + - name: Create new silence + community.grafana.grafana_silence: + comment: "a testcomment" + created_by: "me" + starts_at: "2029-07-29T08:45:45.000Z" + ends_at: "2029-07-29T08:55:45.000Z" + matchers: + - isEqual: true + isRegex: true + name: environment + value: test + state: present + register: result + - assert: + that: + - "result.changed == true" + - "result.failed == false" + - "result.silence.id != ''" + + - name: Check idempotency on silence creation + community.grafana.grafana_silence: + comment: "a testcomment" + created_by: "me" + starts_at: "2029-07-29T08:45:45.000Z" + ends_at: "2029-07-29T08:55:45.000Z" + matchers: + - isEqual: true + isRegex: true + name: environment + value: test + state: present + register: result + - assert: + that: + - "result.changed == false" + - "result.msg != ''" + + - name: Delete the silence + community.grafana.grafana_silence: + comment: "a testcomment" + created_by: "me" + starts_at: "2029-07-29T08:45:45.000Z" + ends_at: "2029-07-29T08:55:45.000Z" + matchers: + - isEqual: true + isRegex: true + name: environment + value: test + state: absent + register: result + - assert: + that: + - "result.changed == true" + - "result.failed == false" + - "result.silence.id != ''" + - - "result.silence.createdBy != 'me'" + + - name: Check idempotency on silence deletion + community.grafana.grafana_silence: + comment: "a testcomment" + created_by: "me" + starts_at: "2029-07-29T08:45:45.000Z" + ends_at: "2029-07-29T08:55:45.000Z" + matchers: + - isEqual: true + isRegex: true + name: environment + value: test + state: absent + register: result + ignore_errors: yes + - assert: + that: + - "result.changed == false" + - "result.failed == false" + - "result.msg == 'Silence does not exist'" diff --git a/tests/integration/targets/grafana_silence/tasks/main.yml b/tests/integration/targets/grafana_silence/tasks/main.yml index ad4cfa25..f8756077 100644 --- a/tests/integration/targets/grafana_silence/tasks/main.yml +++ b/tests/integration/targets/grafana_silence/tasks/main.yml @@ -1,89 +1,6 @@ --- -- name: Create new silence - community.grafana.grafana_silence: - url: "{{ grafana_url }}" - url_username: "{{ grafana_username }}" - url_password: "{{ grafana_password }}" - comment: "a testcomment" - created_by: "me" - starts_at: "2029-07-29T08:45:45.000Z" - ends_at: "2029-07-29T08:55:45.000Z" - matchers: - - isEqual: true - isRegex: true - name: environment - value: test - state: present - register: result -- assert: - that: - - "result.changed == true" - - "result.failed == false" - - "result.silence.id != ''" +- name: Silence creation and deletion + ansible.builtin.include_tasks: create-delete.yml -- name: Check idempotency on silence creation - community.grafana.grafana_silence: - url: "{{ grafana_url }}" - url_username: "{{ grafana_username }}" - url_password: "{{ grafana_password }}" - comment: "a testcomment" - created_by: "me" - starts_at: "2029-07-29T08:45:45.000Z" - ends_at: "2029-07-29T08:55:45.000Z" - matchers: - - isEqual: true - isRegex: true - name: environment - value: test - state: present - register: result -- assert: - that: - - "result.changed == false" - - "result.msg != ''" - -- name: Delete the silence - community.grafana.grafana_silence: - url: "{{ grafana_url }}" - url_username: "{{ grafana_username }}" - url_password: "{{ grafana_password }}" - comment: "a testcomment" - created_by: "me" - starts_at: "2029-07-29T08:45:45.000Z" - ends_at: "2029-07-29T08:55:45.000Z" - matchers: - - isEqual: true - isRegex: true - name: environment - value: test - state: absent - register: result -- assert: - that: - - "result.changed == true" - - "result.failed == false" - - "result.silence.id != ''" - - - "result.silence.createdBy != 'me'" - -- name: Check idempotency on silence deletion - community.grafana.grafana_silence: - url: "{{ grafana_url }}" - url_username: "{{ grafana_username }}" - url_password: "{{ grafana_password }}" - comment: "a testcomment" - created_by: "me" - starts_at: "2029-07-29T08:45:45.000Z" - ends_at: "2029-07-29T08:55:45.000Z" - matchers: - - isEqual: true - isRegex: true - name: environment - value: test - state: absent - register: result - ignore_errors: yes -- assert: - that: - - "result.changed == false" - - "result.failed == false" - - "result.msg == 'Silence does not exist'" \ No newline at end of file +- name: Silence creation and deletion for organization + ansible.builtin.include_tasks: org.yml diff --git a/tests/integration/targets/grafana_silence/tasks/org.yml b/tests/integration/targets/grafana_silence/tasks/org.yml new file mode 100644 index 00000000..514232f9 --- /dev/null +++ b/tests/integration/targets/grafana_silence/tasks/org.yml @@ -0,0 +1,7 @@ +--- +- module_defaults: + community.grafana.grafana_silence: + org_name: Main Org. + block: + - name: Silence creation and deletion + ansible.builtin.include_tasks: create-delete.yml From 747034109bd0a6853331c604875c708c794ec42b Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 10:09:09 +0200 Subject: [PATCH 09/10] docs: org name type --- plugins/modules/grafana_silence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 53dea9cc..7d0421ec 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -42,6 +42,7 @@ - The Grafana organization name where the silence will be created or deleted. - Not used when I(grafana_api_key) is set, because the grafana_api_key only belongs to one organization. - Mutually exclusive with C(org_id). + type: str comment: description: - The comment that describes the silence. From 2a031d0e91dd2403fb5b63addee0fa8926ae704d Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Mon, 27 May 2024 10:24:45 +0200 Subject: [PATCH 10/10] test: mock test called once with to with --- .../modules/grafana/grafana_silence/test_grafana_silence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py b/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py index 96522c2a..c2bb1df3 100644 --- a/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py +++ b/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py @@ -136,7 +136,7 @@ def test_create_silence_new_silence( } ], ) - mock_fetch_url.assert_called_once_with( + mock_fetch_url.assert_called_with( module, "https://grafana.example.com/api/alertmanager/grafana/api/v2/silences", data=json.dumps( @@ -198,7 +198,7 @@ def test_delete_silence(self, mock_fetch_url, mock_get_version): grafana_iface = grafana_silence.GrafanaSilenceInterface(module) silence_id = "470b7116-8f06-4bb6-9e6c-6258aa92218e" result = grafana_iface.delete_silence(silence_id) - mock_fetch_url.assert_called_once_with( + mock_fetch_url.assert_called_with( module, "https://grafana.example.com/api/alertmanager/grafana/api/v2/silence/470b7116-8f06-4bb6-9e6c-6258aa92218e", data=None,