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 diff --git a/plugins/modules/grafana_silence.py b/plugins/modules/grafana_silence.py index 1b2b7091..515a090a 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,22 @@ 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 +218,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): @@ -225,6 +249,11 @@ def __init__(self, module): 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: data = json.dumps(data) @@ -268,6 +297,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 +319,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 +336,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 +352,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 +387,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"), ) 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 6ce81624..bfdb1e0e 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") @@ -164,6 +169,96 @@ def test_create_silence_new_silence( ) self.assertEqual(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.datasource_by_name" + ) + @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, + mock_datasource_by_name, + ): + 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_datasource_by_name.return_value = get_datasource_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/AB981B38A76F/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" ) @@ -208,4 +303,57 @@ 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.datasource_by_name" + ) + @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, mock_datasource_by_name + ): + 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() + 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/AB981B38A76F/api/v2/silence/470b7116-8f06-4bb6-9e6c-6258aa92218e", + data=None, + headers={ + "Content-Type": "application/json", + "Authorization": self.authorization, + }, + method="DELETE", + ) self.assertEqual(result, {"message": "silence deleted"})