From 4c7340ceaae96ac9365663dc5190d99c32b75ab1 Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Fri, 4 Oct 2024 09:24:49 +0100 Subject: [PATCH 1/8] adding ability to target alertmanager datasources for silence creation / deletion --- plugins/modules/grafana_silence.py | 55 +++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 1b2b7091..5472a36e 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -69,6 +69,12 @@ type: list elements: dict required: true + alertmanager_datasource: + description: + - Which alertmanager datasource to target + type: str + required false + default: grafana state: description: - Delete the first occurrence of a silence with the same settings. Can be "absent" or "present". @@ -104,6 +110,23 @@ value: test state: present +- name: Create a silence against a specific datasource + 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" + matchers: + - isEqual: true + isRegex: true + name: environment + value: test + alertmanager_datasource: exampleDS + state: present + + - name: Delete a silence community.grafana.grafana_silence: grafana_url: "https://grafana.example.com" @@ -196,6 +219,8 @@ def __init__(self, module): self._module = module self.grafana_url = base.clean_url(module.params.get("url")) self.org_id = None + # Default here because you can't look up "grafana" DS via API + self.alertmanager_path = "grafana" # {{{ Authentication header self.headers = {"Content-Type": "application/json"} if module.params.get("grafana_api_key", None): @@ -224,6 +249,11 @@ def __init__(self, module): failed=True, msg="Silences API is available starting with Grafana v8", ) + + if module.params.get("alertmanager_datasource", None): + self.alertmanager_path = ( + self.datasource_by_name(module.params["alertmanager_datasource"]) + ) def _send_request(self, url, data=None, headers=None, method="GET"): if data is not None: @@ -268,6 +298,16 @@ def organization_by_name(self, org_name): failed=True, msg="Current user isn't member of organization: %s" % org_name ) + def datasource_by_name(self, datasource_name): + url = f"/api/datasources/name/{datasource_name}" + datasource = self._send_request(url, headers=self.headers, method="GET") + if datasource: + return datasource["uid"] + + return self._module.fail_json( + failed=True, msg=f"Datasource not found: {datasource_name}" + ) + def get_version(self): url = "/api/health" response = self._send_request( @@ -280,7 +320,7 @@ def get_version(self): raise GrafanaError("Failed to retrieve version from '%s'" % url) def create_silence(self, comment, created_by, starts_at, ends_at, matchers): - url = "/api/alertmanager/grafana/api/v2/silences" + url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences" silence = dict( comment=comment, createdBy=created_by, @@ -297,7 +337,7 @@ def create_silence(self, comment, created_by, starts_at, ends_at, matchers): return response def get_silence(self, comment, created_by, starts_at, ends_at, matchers): - url = "/api/alertmanager/grafana/api/v2/silences" + url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences" responses = self._send_request(url, headers=self.headers, method="GET") @@ -313,21 +353,17 @@ def get_silence(self, comment, created_by, starts_at, ends_at, matchers): return None def get_silence_by_id(self, silence_id): - url = "/api/alertmanager/grafana/api/v2/silence/{SilenceId}".format( - SilenceId=silence_id - ) + url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silence/{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" + url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences" response = self._send_request(url, headers=self.headers, method="GET") return response def delete_silence(self, silence_id): - url = "/api/alertmanager/grafana/api/v2/silence/{SilenceId}".format( - SilenceId=silence_id - ) + url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silence/{silence_id}" response = self._send_request(url, headers=self.headers, method="DELETE") return response @@ -352,6 +388,7 @@ def setup_module_object(): org_name=dict(type="str"), skip_version_check=dict(type="bool", default=False), starts_at=dict(type="str", required=True), + alertmanager_datasource=dict(type=str), state=dict(type="str", choices=["present", "absent"], default="present"), ) From a13d474a996341c4e5c90ae53daa53ea66cc9a34 Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Fri, 4 Oct 2024 09:27:16 +0100 Subject: [PATCH 2/8] add tests for targeting alertmanager datasource for silences --- .../grafana_silence/test_grafana_silence.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) 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 c2bb1df3..1cbfda3e 100644 --- a/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py +++ b/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py @@ -164,6 +164,89 @@ def test_create_silence_new_silence( ) self.assertEquals(result, {"silenceID": "470b7116-8f06-4bb6-9e6c-6258aa92218e"}) + # create a new silence with alertmanager datasource defined + @patch( + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_silence" + ) + @patch( + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" + ) + @patch( + "ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url" + ) + def test_create_silence_new_silence_with_datasource( + self, mock_fetch_url, mock_get_version, mock_get_silence + ): + set_module_args( + { + "url": "https://grafana.example.com", + "url_username": "admin", + "url_password": "changeme", + "comment": "a testcomment", + "created_by": "me", + "starts_at": "2029-07-29T08:45:45.000Z", + "ends_at": "2029-07-29T08:55:45.000Z", + "alertmanager_datasource": "testds", + "matchers": [ + { + "isEqual": True, + "isRegex": True, + "name": "environment", + "value": "test", + } + ], + "state": "present", + } + ) + module = grafana_silence.setup_module_object() + mock_get_version.return_value = get_version_resp() + mock_fetch_url.return_value = silence_created_resp() + mock_get_silence.return_value = silence_get_resp() + + grafana_iface = grafana_silence.GrafanaSilenceInterface(module) + result = grafana_iface.create_silence( + "a testcomment", + "me", + "2029-07-29T08:45:45.000Z", + "2029-07-29T08:55:45.000Z", + [ + { + "isEqual": True, + "isRegex": True, + "name": "environment", + "value": "test", + } + ], + ) + mock_fetch_url.assert_called_with( + module, + "https://grafana.example.com/api/alertmanager/testds/api/v2/silences", + data=json.dumps( + { + "comment": "a testcomment", + "createdBy": "me", + "startsAt": "2029-07-29T08:45:45.000Z", + "endsAt": "2029-07-29T08:55:45.000Z", + "matchers": [ + { + "isEqual": True, + "isRegex": True, + "name": "environment", + "value": "test", + } + ], + }, + sort_keys=True, + ), + headers={ + "Content-Type": "application/json", + "Authorization": self.authorization, + }, + method="POST", + ) + self.assertEquals(result, {"silenceID": "470b7116-8f06-4bb6-9e6c-6258aa92218e"}) + + @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" ) @@ -209,3 +292,50 @@ def test_delete_silence(self, mock_fetch_url, mock_get_version): method="DELETE", ) self.assertEquals(result, {"message": "silence deleted"}) + + @patch( + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" + ) + @patch( + "ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url" + ) + def test_delete_silence_with_datasource(self, mock_fetch_url, mock_get_version): + set_module_args( + { + "url": "https://grafana.example.com", + "url_username": "admin", + "url_password": "changeme", + "comment": "a testcomment", + "created_by": "me", + "ends_at": "2029-07-29T08:55:45.000Z", + "alertmanager_datasource": "testds", + "matchers": [ + { + "isEqual": True, + "isRegex": True, + "name": "environment", + "value": "test", + } + ], + "starts_at": "2029-07-29T08:45:45.000Z", + "state": "present", + } + ) + module = grafana_silence.setup_module_object() + mock_fetch_url.return_value = silence_deleted_resp() + mock_get_version.return_value = get_version_resp() + + 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_with( + module, + "https://grafana.example.com/api/alertmanager/testds/api/v2/silence/470b7116-8f06-4bb6-9e6c-6258aa92218e", + data=None, + headers={ + "Content-Type": "application/json", + "Authorization": self.authorization, + }, + method="DELETE", + ) + self.assertEquals(result, {"message": "silence deleted"}) From 88b46d024760d252c5dc496e669129c2f68455df Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Fri, 4 Oct 2024 14:36:31 +0100 Subject: [PATCH 3/8] add changelog fragment --- changelogs/fragments/605-silence-datastore-feature.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/605-silence-datastore-feature.yml diff --git a/changelogs/fragments/605-silence-datastore-feature.yml b/changelogs/fragments/605-silence-datastore-feature.yml new file mode 100644 index 00000000..abbe9248 --- /dev/null +++ b/changelogs/fragments/605-silence-datastore-feature.yml @@ -0,0 +1,2 @@ +minor_changes: + - Add the ability to define which alertmanager datasource to target in grafana_silence From 539d502955027688458d4a252a91d2ebfc11daa3 Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Fri, 4 Oct 2024 14:46:29 +0100 Subject: [PATCH 4/8] fix documentation lint --- plugins/modules/grafana_silence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 5472a36e..4d5b0baf 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -73,7 +73,7 @@ description: - Which alertmanager datasource to target type: str - required false + required: false default: grafana state: description: @@ -251,8 +251,8 @@ def __init__(self, module): ) if module.params.get("alertmanager_datasource", None): - self.alertmanager_path = ( - self.datasource_by_name(module.params["alertmanager_datasource"]) + self.alertmanager_path = self.datasource_by_name( + module.params["alertmanager_datasource"] ) def _send_request(self, url, data=None, headers=None, method="GET"): From fd004e182ef81f6bac59f53c475a27860d52bea2 Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Wed, 9 Oct 2024 08:44:35 +0100 Subject: [PATCH 5/8] add mocks for datasource_by_name --- .../grafana_silence/test_grafana_silence.py | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 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 1cbfda3e..716e5fdf 100644 --- a/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py +++ b/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py @@ -74,6 +74,11 @@ def get_version_resp(): return {"major": 10, "minor": 0, "rev": 0} +def get_datasource_resp(): + server_response = json.dumps({"uid": "AB981B38A76F"}) + return (MockedReponse(server_response), {"status": 200}) + + class GrafanaSilenceTest(TestCase): def setUp(self): self.authorization = basic_auth_header("admin", "changeme") @@ -171,11 +176,18 @@ def test_create_silence_new_silence( @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" ) + @patch( + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name" + ) @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url" ) def test_create_silence_new_silence_with_datasource( - self, mock_fetch_url, mock_get_version, mock_get_silence + self, + mock_fetch_url, + mock_get_version, + mock_get_silence, + mock_datasource_by_name, ): set_module_args( { @@ -200,6 +212,7 @@ def test_create_silence_new_silence_with_datasource( ) module = grafana_silence.setup_module_object() mock_get_version.return_value = get_version_resp() + mock_datasource_by_name.return_value = get_datasource_resp() mock_fetch_url.return_value = silence_created_resp() mock_get_silence.return_value = silence_get_resp() @@ -220,7 +233,7 @@ def test_create_silence_new_silence_with_datasource( ) mock_fetch_url.assert_called_with( module, - "https://grafana.example.com/api/alertmanager/testds/api/v2/silences", + "https://grafana.example.com/api/alertmanager/AB981B38A76F/api/v2/silences", data=json.dumps( { "comment": "a testcomment", @@ -246,7 +259,6 @@ def test_create_silence_new_silence_with_datasource( ) self.assertEquals(result, {"silenceID": "470b7116-8f06-4bb6-9e6c-6258aa92218e"}) - @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" ) @@ -296,10 +308,15 @@ def test_delete_silence(self, mock_fetch_url, mock_get_version): @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" ) + @patch( + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name" + ) @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url" ) - def test_delete_silence_with_datasource(self, mock_fetch_url, mock_get_version): + def test_delete_silence_with_datasource( + self, mock_fetch_url, mock_get_version, mock_datasource_by_name + ): set_module_args( { "url": "https://grafana.example.com", @@ -324,13 +341,14 @@ def test_delete_silence_with_datasource(self, mock_fetch_url, mock_get_version): module = grafana_silence.setup_module_object() mock_fetch_url.return_value = silence_deleted_resp() mock_get_version.return_value = get_version_resp() + mock_datasource_by_name.return_value = get_datasource_resp() 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_with( module, - "https://grafana.example.com/api/alertmanager/testds/api/v2/silence/470b7116-8f06-4bb6-9e6c-6258aa92218e", + "https://grafana.example.com/api/alertmanager/AB981B38A76F/api/v2/silence/470b7116-8f06-4bb6-9e6c-6258aa92218e", data=None, headers={ "Content-Type": "application/json", From e59c96a283c819cc0a958d93955e91437beb7023 Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Wed, 9 Oct 2024 08:44:57 +0100 Subject: [PATCH 6/8] black formatting --- plugins/modules/grafana_silence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 4d5b0baf..d1142738 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -249,10 +249,10 @@ def __init__(self, module): failed=True, msg="Silences API is available starting with Grafana v8", ) - + if module.params.get("alertmanager_datasource", None): self.alertmanager_path = self.datasource_by_name( - module.params["alertmanager_datasource"] + module.params["alertmanager_datasource"] ) def _send_request(self, url, data=None, headers=None, method="GET"): From d29a845764e6e06eec081cd7730062c463becf3c Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Wed, 9 Oct 2024 08:51:33 +0100 Subject: [PATCH 7/8] fix mock ordering --- .../grafana/grafana_silence/test_grafana_silence.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 716e5fdf..df28ecc2 100644 --- a/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py +++ b/tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py @@ -171,13 +171,13 @@ def test_create_silence_new_silence( # create a new silence with alertmanager datasource defined @patch( - "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_silence" + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name" ) @patch( - "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_silence" ) @patch( - "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name" + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" ) @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url" @@ -306,10 +306,10 @@ def test_delete_silence(self, mock_fetch_url, mock_get_version): self.assertEquals(result, {"message": "silence deleted"}) @patch( - "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name" ) @patch( - "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name" + "ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version" ) @patch( "ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url" From 1dcea66ff4a7f0c343a77366748e9104e76e27bb Mon Sep 17 00:00:00 2001 From: Danny Webb Date: Wed, 9 Oct 2024 10:01:22 +0100 Subject: [PATCH 8/8] remove extraneous newline --- plugins/modules/grafana_silence.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index d1142738..515a090a 100644 --- a/plugins/modules/grafana_silence.py +++ b/plugins/modules/grafana_silence.py @@ -126,7 +126,6 @@ alertmanager_datasource: exampleDS state: present - - name: Delete a silence community.grafana.grafana_silence: grafana_url: "https://grafana.example.com"