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` diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 35f54a3d..7d0421ec 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -30,6 +30,19 @@ 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). + type: str comment: description: - The comment that describes the silence. @@ -181,9 +194,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"] @@ -192,8 +206,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() @@ -233,6 +253,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( @@ -310,23 +345,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 @@ -338,7 +374,6 @@ def main(): ) if state == "present": - if not silence: silence = grafana_iface.create_silence( comment, created_by, starts_at, ends_at, matchers @@ -362,6 +397,7 @@ def main(): changed=changed, msg="Silence does not exist", ) + module.exit_json(failed=failed, changed=changed, silence=silence) diff --git a/roles/grafana/README.md b/roles/grafana/README.md index 6c1e0dd4..09901eff 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 | | [**grafana_contact_point**](https://docs.ansible.com/ansible/latest/collections/community/grafana/grafana_contact_point_module.html) | diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml index 4b0918f9..347ef1c6 100644 --- a/roles/grafana/tasks/main.yml +++ b/roles/grafana/tasks/main.yml @@ -221,9 +221,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} 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 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,