From e98a4d11af988c054fe9e5fb10f5152237dcf028 Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:57:24 +0200 Subject: [PATCH 1/7] docs: id and duration args --- plugins/modules/grafana_silence.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 1b2b7091..85a5831b 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -61,8 +61,13 @@ ends_at: description: - ISO 8601 Timestamp with milliseconds e.g. "2029-07-29T08:45:45.000Z" when the silence will end. + - Mutually exclusive with C(duration). + type: str + duration: + description: + - Duration for the silence in ISO 8601 duration format e.g. "PT10M" for 10 minutes. + - Mutually exclusive with C(ends_at). type: str - required: true matchers: description: - List of matchers to select which alerts are affected by the silence. @@ -75,6 +80,10 @@ default: present type: str choices: ["present", "absent"] + id: + description: + - The id of the silence. + type: str skip_version_check: description: - Skip Grafana version check and try to reach api endpoint anyway. From 5321b903c473ab37ef30fba3f695c789e071bced Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:58:25 +0200 Subject: [PATCH 2/7] docs: enhanced examples with new args --- plugins/modules/grafana_silence.py | 44 ++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 85a5831b..4b7807e0 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -98,14 +98,14 @@ EXAMPLES = """ --- -- name: Create a silence +- name: Create a silence with duration community.grafana.grafana_silence: grafana_url: "https://grafana.example.com" grafana_api_key: "{{ some_api_token_value }}" comment: "a testcomment" created_by: "me" starts_at: "2029-07-29T08:45:45.000Z" - ends_at: "2029-07-29T08:55:45.000Z" + duration: "PT10M" matchers: - isEqual: true isRegex: true @@ -113,7 +113,37 @@ value: test state: present -- name: Delete a silence +- name: Delete silence with duration without specifying id + community.grafana.grafana_silence: + grafana_url: "https://grafana.example.com" + grafana_api_key: "{{ some_api_token_value }}" + comment: "a testcomment" + created_by: "me" + starts_at: "2029-07-29T08:45:45.000Z" + duration: "PT10M" + matchers: + - isEqual: true + isRegex: true + name: environment + value: test + state: absent + +- name: Delete silence without specifying id + community.grafana.grafana_silence: + grafana_url: "https://grafana.example.com" + grafana_api_key: "{{ some_api_token_value }}" + comment: "a testcomment" + created_by: "me" + starts_at: "2029-07-29T08:45:45.000Z" + starts_at: "2029-07-29T08:55:45.000Z" + matchers: + - isEqual: true + isRegex: true + name: environment + value: test + state: absent + +- name: Create a silence with specified id community.grafana.grafana_silence: grafana_url: "https://grafana.example.com" grafana_api_key: "{{ some_api_token_value }}" @@ -126,6 +156,14 @@ isRegex: true name: environment value: test + id: "custom-silence-id" + state: present + +- name: Delete a silence by id + community.grafana.grafana_silence: + grafana_url: "https://grafana.example.com" + grafana_api_key: "{{ some_api_token_value }}" + id: "custom-silence-id" state: absent """ From c2423a9b992eed5e62f6fea02a02c7bfe3776c86 Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Tue, 2 Jul 2024 09:59:39 +0200 Subject: [PATCH 3/7] chore: new module args --- plugins/modules/grafana_silence.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 4b7807e0..78789987 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -393,7 +393,9 @@ def setup_module_object(): argument_spec.update( comment=dict(type="str", required=True), created_by=dict(type="str", required=True), - ends_at=dict(type="str", required=True), + duration=dict(type="str"), + ends_at=dict(type="str"), + id=dict(type="str"), matchers=dict(type="list", elements="dict", required=True), org_id=dict(default=1, type="int"), org_name=dict(type="str"), @@ -407,8 +409,10 @@ def main(): module = setup_module_object() comment = module.params["comment"] created_by = module.params["created_by"] - ends_at = module.params["ends_at"] + duration = module.params.get("duration") + ends_at = module.params.get("ends_at") matchers = module.params["matchers"] + silence_id = module.params.get("id") starts_at = module.params["starts_at"] state = module.params["state"] From 575d8b21729a02dcd6de5bb675c3c937b2ff8ffc Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:26:12 +0200 Subject: [PATCH 4/7] docs: examples arg name fixed --- plugins/modules/grafana_silence.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 78789987..fa58eb75 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -102,7 +102,7 @@ community.grafana.grafana_silence: grafana_url: "https://grafana.example.com" grafana_api_key: "{{ some_api_token_value }}" - comment: "a testcomment" + comment: "a test comment" created_by: "me" starts_at: "2029-07-29T08:45:45.000Z" duration: "PT10M" @@ -117,7 +117,7 @@ community.grafana.grafana_silence: grafana_url: "https://grafana.example.com" grafana_api_key: "{{ some_api_token_value }}" - comment: "a testcomment" + comment: "a test comment" created_by: "me" starts_at: "2029-07-29T08:45:45.000Z" duration: "PT10M" @@ -132,10 +132,10 @@ community.grafana.grafana_silence: grafana_url: "https://grafana.example.com" grafana_api_key: "{{ some_api_token_value }}" - comment: "a testcomment" + comment: "a test comment" created_by: "me" starts_at: "2029-07-29T08:45:45.000Z" - starts_at: "2029-07-29T08:55:45.000Z" + ends_at: "2029-07-29T08:55:45.000Z" matchers: - isEqual: true isRegex: true @@ -147,7 +147,7 @@ community.grafana.grafana_silence: grafana_url: "https://grafana.example.com" grafana_api_key: "{{ some_api_token_value }}" - comment: "a testcomment" + comment: "a test comment" created_by: "me" starts_at: "2029-07-29T08:45:45.000Z" ends_at: "2029-07-29T08:55:45.000Z" From b1cecfd62516e121b8215848d15b6d98752ca85f Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:50:47 +0200 Subject: [PATCH 5/7] refactor: silence payload with new id args --- plugins/modules/grafana_silence.py | 96 +++++++++++++----------------- 1 file changed, 43 insertions(+), 53 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index fa58eb75..cc8706d8 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -326,15 +326,8 @@ def get_version(self): return {"major": int(major), "minor": int(minor), "rev": int(rev)} raise GrafanaError("Failed to retrieve version from '%s'" % url) - def create_silence(self, comment, created_by, starts_at, ends_at, matchers): + def create_silence(self, silence): url = "/api/alertmanager/grafana/api/v2/silences" - silence = dict( - comment=comment, - createdBy=created_by, - endsAt=ends_at, - matchers=matchers, - startsAt=starts_at, - ) response = self._send_request( url, data=silence, headers=self.headers, method="POST" ) @@ -343,33 +336,25 @@ def create_silence(self, comment, created_by, starts_at, ends_at, matchers): response.pop("id", None) return response - def get_silence(self, comment, created_by, starts_at, ends_at, matchers): - url = "/api/alertmanager/grafana/api/v2/silences" - - responses = self._send_request(url, headers=self.headers, method="GET") - - for response in responses: - if ( - response["comment"] == comment - and response["createdBy"] == created_by - and response["startsAt"] == starts_at - and response["endsAt"] == ends_at - and response["matchers"] == matchers - ): - return response - return None - - def get_silence_by_id(self, silence_id): - url = "/api/alertmanager/grafana/api/v2/silence/{SilenceId}".format( - SilenceId=silence_id - ) - response = self._send_request(url, headers=self.headers, method="GET") - return response - - def get_silences(self): - url = "/api/alertmanager/grafana/api/v2/silences" - response = self._send_request(url, headers=self.headers, method="GET") - return response + def get_silence(self, silence): + if silence["silenceID"]: + url = "/api/alertmanager/grafana/api/v2/silence/%s" % silence["silenceID"] + response = self._send_request(url, headers=self.headers, method="GET") + return response + else: + url = "/api/alertmanager/grafana/api/v2/silences" + response = self._send_request(url, headers=self.headers, method="GET") + + for resp in response: + if ( + resp["comment"] == silence["comment"] + and resp["createdBy"] == silence["createdBy"] + and resp["startsAt"] == silence["startsAt"] + and resp["endsAt"] == silence["endsAt"] + and resp["matchers"] == silence["matchers"] + ): + return resp + return None def delete_silence(self, silence_id): url = "/api/alertmanager/grafana/api/v2/silence/{SilenceId}".format( @@ -384,7 +369,10 @@ def setup_module_object(): argument_spec=argument_spec, supports_check_mode=False, required_together=base.grafana_required_together(), - mutually_exclusive=base.grafana_mutually_exclusive(), + mutually_exclusive=base.grafana_mutually_exclusive() + + [ + ["ends_at", "duration"], + ], ) return module @@ -407,29 +395,31 @@ def setup_module_object(): def main(): module = setup_module_object() - comment = module.params["comment"] - created_by = module.params["created_by"] - duration = module.params.get("duration") - ends_at = module.params.get("ends_at") - matchers = module.params["matchers"] - silence_id = module.params.get("id") - starts_at = module.params["starts_at"] - state = module.params["state"] - + grafana_iface = GrafanaSilenceInterface(module) changed = False failed = False - grafana_iface = GrafanaSilenceInterface(module) - silence = grafana_iface.get_silence( - comment, created_by, starts_at, ends_at, matchers - ) + silence_payload = { + "comment": module.params.get("comment"), + "createdBy": module.params.get("created_by"), + "matchers": module.params.get("matchers"), + "startsAt": module.params.get("starts_at"), + "silenceID": module.params.get("silence_id"), + } + + if module.params.get("ends_at"): + silence_payload["endsAt"] = module.params.get("ends_at") + elif module.params.get("duration"): + silence_payload["duration"] = module.params.get("duration") + + silence = grafana_iface.get_silence(silence_payload) + + state = module.params.get("state") if state == "present": if not silence: - silence = grafana_iface.create_silence( - comment, created_by, starts_at, ends_at, matchers - ) - silence = grafana_iface.get_silence_by_id(silence["silenceID"]) + silence = grafana_iface.create_silence(silence_payload) + silence = grafana_iface.get_silence(silence_payload) changed = True else: module.exit_json( From 2d78bdd187d2b29b6a93a8266cf386d15b146d49 Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:09:30 +0200 Subject: [PATCH 6/7] feat: duration calc --- plugins/modules/grafana_silence.py | 32 +++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index cc8706d8..8cdcfffb 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -225,6 +225,7 @@ """ import json +from datetime import datetime, timedelta from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url, basic_auth_header @@ -393,6 +394,18 @@ def setup_module_object(): ) +def parse_iso8601_duration(duration): + # Remove the leading 'P' and split days ('D'), hours ('H'), minutes ('M') + duration = duration[1:] # Remove 'P' + days, time = duration.split("T") if "T" in duration else (None, duration) + + days = int(days[:-1]) if days and "D" in days else 0 + hours = int(time.split("H")[0][:-1]) if "H" in time else 0 + minutes = int(time.split("M")[0][-1:]) if "M" in time else 0 + + return timedelta(days=days, hours=hours, minutes=minutes) + + def main(): module = setup_module_object() grafana_iface = GrafanaSilenceInterface(module) @@ -407,14 +420,27 @@ def main(): "silenceID": module.params.get("silence_id"), } + state = module.params.get("state") + if module.params.get("ends_at"): silence_payload["endsAt"] = module.params.get("ends_at") elif module.params.get("duration"): - silence_payload["duration"] = module.params.get("duration") + starts_at = module.params.get("starts_at") + duration = module.params.get("duration") - silence = grafana_iface.get_silence(silence_payload) + # Parse starts_at into datetime object + starts_at_dt = datetime.strptime(starts_at, "%Y-%m-%dT%H:%M:%S.%fZ") - state = module.params.get("state") + # Parse ISO 8601 duration into timedelta + duration_timedelta = parse_iso8601_duration(duration) + + # Calculate ends_at + ends_at_dt = starts_at_dt + duration_timedelta + + # Format ends_at as ISO 8601 + silence_payload["endsAt"] = ends_at_dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + silence = grafana_iface.get_silence(silence_payload) if state == "present": if not silence: From 598da04591d356bc29fed448b51494eeb0def58a Mon Sep 17 00:00:00 2001 From: Nemental <15136847+Nemental@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:07:49 +0200 Subject: [PATCH 7/7] test: duration integration tests --- .../grafana_silence/tasks/create-delete.yml | 6 +- .../grafana_silence/tasks/duration.yml | 83 +++++++++++++++++++ .../targets/grafana_silence/tasks/main.yml | 9 +- 3 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 tests/integration/targets/grafana_silence/tasks/duration.yml diff --git a/tests/integration/targets/grafana_silence/tasks/create-delete.yml b/tests/integration/targets/grafana_silence/tasks/create-delete.yml index 1561eb59..bd17f9da 100644 --- a/tests/integration/targets/grafana_silence/tasks/create-delete.yml +++ b/tests/integration/targets/grafana_silence/tasks/create-delete.yml @@ -23,7 +23,7 @@ - "result.changed == true" - "result.failed == false" - "result.silence.id != ''" - + - name: Check idempotency on silence creation community.grafana.grafana_silence: comment: "a testcomment" @@ -41,7 +41,7 @@ that: - "result.changed == false" - "result.msg != ''" - + - name: Delete the silence community.grafana.grafana_silence: comment: "a testcomment" @@ -61,7 +61,7 @@ - "result.failed == false" - "result.silence.id != ''" - - "result.silence.createdBy != 'me'" - + - name: Check idempotency on silence deletion community.grafana.grafana_silence: comment: "a testcomment" diff --git a/tests/integration/targets/grafana_silence/tasks/duration.yml b/tests/integration/targets/grafana_silence/tasks/duration.yml new file mode 100644 index 00000000..3770a0a3 --- /dev/null +++ b/tests/integration/targets/grafana_silence/tasks/duration.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" + duration: "PT17D21H256M387S" + 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" + duration: "PT17D21H256M387S" + 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" + duration: "PT17D21H256M387S" + 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" + duration: "PT17D21H256M387S" + 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 f8756077..9ab61381 100644 --- a/tests/integration/targets/grafana_silence/tasks/main.yml +++ b/tests/integration/targets/grafana_silence/tasks/main.yml @@ -1,6 +1,7 @@ --- - name: Silence creation and deletion - ansible.builtin.include_tasks: create-delete.yml - -- name: Silence creation and deletion for organization - ansible.builtin.include_tasks: org.yml + ansible.builtin.include_tasks: "{{ item ~ '.yml' }}" + loop: + - create-delete + - org + - duration