Skip to content

Commit

Permalink
Merge branch 'main' into auto-bump-grafana
Browse files Browse the repository at this point in the history
  • Loading branch information
Nemental authored Apr 22, 2024
2 parents 66f5d5e + 4d0af1a commit 04b9903
Show file tree
Hide file tree
Showing 8 changed files with 688 additions and 0 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/add grafana_silence module.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
major_changes:
- grafana_silence ; adding new module to create and delete silences through the API
1 change: 1 addition & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ action_groups:
- grafana_plugin
- grafana_team
- grafana_user
- grafana_silence
369 changes: 369 additions & 0 deletions plugins/modules/grafana_silence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright: (c) 2023, flkhndlr (@flkhndlr)

from __future__ import absolute_import, division, print_function

DOCUMENTATION = """
module: grafana_silence
author:
- flkhndlr (@flkhndlr)
version_added: "1.8.0"
short_description: Manage Grafana Silences
description:
- Create/delete Grafana Silences through the Alertmanager Silence API.
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:
comment:
description:
- The comment that describes the silence.
required: true
type: str
created_by:
description:
- The author that creates the silence.
required: true
type: str
starts_at:
description:
- ISO 8601 Timestamp with milliseconds e.g. "2029-07-29T08:45:45.000Z" when the silence starts.
type: str
required: true
ends_at:
description:
- ISO 8601 Timestamp with milliseconds e.g. "2029-07-29T08:45:45.000Z" when the silence will end.
type: str
required: true
matchers:
description:
- List of matchers to select which alerts are affected by the silence.
type: list
elements: dict
required: true
state:
description:
- Delete the first occurrence of a silence with the same settings. Can be "absent" or "present".
default: present
type: str
choices: ["present", "absent"]
skip_version_check:
description:
- Skip Grafana version check and try to reach api endpoint anyway.
- This parameter can be useful if you enabled `hide_version` in grafana.ini
required: False
type: bool
default: False
extends_documentation_fragment:
- community.grafana.basic_auth
- community.grafana.api_key
"""

EXAMPLES = """
---
- name: Create a silence
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
state: present
- name: Delete a silence
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
state: absent
"""

RETURN = """
---
silence:
description: Information about the silence
returned: On success
type: complex
contains:
id:
description: The id of the silence
returned: success
type: str
sample:
- ec27df6b-ac3c-412f-ae0b-6e3e1f41c9c3
comment:
description: The comment of the silence
returned: success
type: str
sample:
- this is a test
createdBy:
description: The author of the silence
returned: success
type: str
sample:
- me
startsAt:
description: The begin timestamp of the silence
returned: success
type: str
sample:
- "2029-07-29T08:45:45.000Z"
endsAt:
description: The end timestamp of the silence
returned: success
type: str
sample:
- "2029-07-29T08:55:45.000Z"
matchers:
description: The matchers of the silence
returned: success
type: list
sample:
- [{"isEqual": true, "isRegex": true, "name": "environment", "value": "test"}]
status:
description: The status of the silence
returned: success
type: dict
sample:
- {"state": "pending"}
updatedAt:
description: The timestamp of the last update for the silence
returned: success
type: str
sample:
- "2023-07-27T13:27:33.042Z"
"""

import json

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url, basic_auth_header
from ansible.module_utils._text import to_text
from ansible_collections.community.grafana.plugins.module_utils import base

__metaclass__ = type


class GrafanaError(Exception):
pass


class GrafanaSilenceInterface(object):
def __init__(self, module):
self._module = module
# {{{ 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"]
)
else:
self.headers["Authorization"] = basic_auth_header(
module.params["url_username"], module.params["url_password"]
)
# }}}
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()
except GrafanaError as e:
self._module.fail_json(failed=True, msg=to_text(e))
if grafana_version["major"] < 8:
self._module.fail_json(
failed=True,
msg="Silences API is available starting with Grafana v8",
)

def _send_request(self, url, data=None, headers=None, method="GET"):
if data is not None:
data = json.dumps(data)
if not headers:
headers = []

full_url = "{grafana_url}{path}".format(grafana_url=self.grafana_url, path=url)
resp, info = fetch_url(
self._module, full_url, data=data, headers=headers, method=method
)
status_code = info["status"]
if status_code == 404:
return None
elif status_code == 401:
self._module.fail_json(
failed=True,
msg="Unauthorized to perform action '%s' on '%s'" % (method, full_url),
)
elif status_code == 403:
self._module.fail_json(failed=True, msg="Permission Denied")
elif status_code in [200, 202]:
return self._module.from_json(resp.read())
elif status_code == 400:
self._module.fail_json(failed=True, msg=info)
self._module.fail_json(
failed=True, msg="Grafana Silences API answered with HTTP %d" % status_code
)

def get_version(self):
url = "/api/health"
response = self._send_request(
url, data=None, headers=self.headers, method="GET"
)
version = response.get("version")
if version is not None:
major, minor, rev = version.split(".")
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):
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"
)
if self.get_version()["major"] == 8:
response["silenceID"] = response["id"]
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 delete_silence(self, silence_id):
url = "/api/alertmanager/grafana/api/v2/silence/{SilenceId}".format(
SilenceId=silence_id
)
response = self._send_request(url, headers=self.headers, method="DELETE")
return response


def setup_module_object():
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
required_together=base.grafana_required_together(),
mutually_exclusive=base.grafana_mutually_exclusive(),
)
return module


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),
skip_version_check=dict(type="bool", default=False),
)


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"]
state = module.params["state"]

changed = False
failed = False
grafana_iface = GrafanaSilenceInterface(module)

silence = grafana_iface.get_silence(
comment, created_by, starts_at, ends_at, matchers
)

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"])
changed = True
else:
module.exit_json(
failed=failed,
changed=changed,
msg="Silence with same parameters already exists! eg. '%s'"
% silence["id"],
)
elif state == "absent":
if silence:
grafana_iface.delete_silence(silence["id"])
changed = True
else:
module.exit_json(
failed=False,
changed=changed,
msg="Silence does not exist",
)
module.exit_json(failed=failed, changed=changed, silence=silence)


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions tests/integration/targets/grafana_silence/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---

grafana_url: http://grafana:3000/
grafana_username: admin
grafana_password: admin
5 changes: 5 additions & 0 deletions tests/integration/targets/grafana_silence/runme.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

set -eux

ansible-playbook site.yml
6 changes: 6 additions & 0 deletions tests/integration/targets/grafana_silence/site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: Run tests for grafana_silence
hosts: localhost
tasks:
- ansible.builtin.include_role:
name: ../../grafana_silence
Loading

0 comments on commit 04b9903

Please sign in to comment.