From 19a5975181878f99a936ce23be1f38038fe3a2a5 Mon Sep 17 00:00:00 2001 From: Hippolyte HENRY Date: Tue, 15 Dec 2020 19:50:42 +0100 Subject: [PATCH] Add downtime module (#1437) * Add downtime module * review + tests * importorskip + review * py36 * make py3.6+ dep explicit --- plugins/modules/datadog_downtime.py | 1 + .../monitoring/datadog/datadog_downtime.py | 308 ++++++++++++++++++ .../monitoring/test_datadog_downtime.py | 224 +++++++++++++ tests/unit/requirements.txt | 3 + 4 files changed, 536 insertions(+) create mode 120000 plugins/modules/datadog_downtime.py create mode 100644 plugins/modules/monitoring/datadog/datadog_downtime.py create mode 100644 tests/unit/plugins/modules/monitoring/test_datadog_downtime.py diff --git a/plugins/modules/datadog_downtime.py b/plugins/modules/datadog_downtime.py new file mode 120000 index 0000000000..feaa6d50c2 --- /dev/null +++ b/plugins/modules/datadog_downtime.py @@ -0,0 +1 @@ +./monitoring/datadog/datadog_downtime.py \ No newline at end of file diff --git a/plugins/modules/monitoring/datadog/datadog_downtime.py b/plugins/modules/monitoring/datadog/datadog_downtime.py new file mode 100644 index 0000000000..ef308bdabe --- /dev/null +++ b/plugins/modules/monitoring/datadog/datadog_downtime.py @@ -0,0 +1,308 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Datadog, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: datadog_downtime +short_description: Manages Datadog downtimes +version_added: 2.0.0 +description: + - Manages downtimes within Datadog. + - Options as described on U(https://docs.datadoghq.com/api/v1/downtimes/s). +author: + - Datadog (@Datadog) +requirements: + - datadog-api-client + - Python 3.6+ +options: + api_key: + description: + - Your Datadog API key. + required: true + type: str + api_host: + description: + - The URL to the Datadog API. + - This value can also be set with the C(DATADOG_HOST) environment variable. + required: false + default: https://api.datadoghq.com + type: str + app_key: + description: + - Your Datadog app key. + required: true + type: str + state: + description: + - The designated state of the downtime. + required: false + choices: ["present", "absent"] + default: present + type: str + id: + description: + - The identifier of the downtime. + - If empty, a new downtime gets created, otherwise it is either updated or deleted depending of the C(state). + - To keep your playbook idempotent, you should save the identifier in a file and read it in a lookup. + type: int + monitor_tags: + description: + - A list of monitor tags to which the downtime applies. + - The resulting downtime applies to monitors that match ALL provided monitor tags. + type: list + elements: str + scope: + description: + - A list of scopes to which the downtime applies. + - The resulting downtime applies to sources that matches ALL provided scopes. + type: list + elements: str + monitor_id: + description: + - The ID of the monitor to mute. If not provided, the downtime applies to all monitors. + type: int + downtime_message: + description: + - A message to include with notifications for this downtime. + - Email notifications can be sent to specific users by using the same "@username" notation as events. + type: str + start: + type: int + description: + - POSIX timestamp to start the downtime. If not provided, the downtime starts the moment it is created. + end: + type: int + description: + - POSIX timestamp to end the downtime. If not provided, the downtime is in effect until you cancel it. + timezone: + description: + - The timezone for the downtime. + type: str + rrule: + description: + - The C(RRULE) standard for defining recurring events. + - For example, to have a recurring event on the first day of each month, + select a type of rrule and set the C(FREQ) to C(MONTHLY) and C(BYMONTHDAY) to C(1). + - Most common rrule options from the iCalendar Spec are supported. + - Attributes specifying the duration in C(RRULE) are not supported (e.g. C(DTSTART), C(DTEND), C(DURATION)). + type: str +""" + +EXAMPLES = """ + - name: Create a downtime + register: downtime_var + community.general.datadog_downtime: + state: present + monitor_tags: + - "foo:bar" + downtime_message: "Downtime for foo:bar" + scope: "test" + api_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + app_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + # Lookup the id in the file and ignore errors if the file doesn't exits, so downtime gets created + id: "{{ lookup('file', inventory_hostname ~ '_downtime_id.txt', errors='ignore') }}" + - name: Save downtime id to file for later updates and idempotence + delegate_to: localhost + copy: + content: "{{ downtime.downtime.id }}" + dest: "{{ inventory_hostname ~ '_downtime_id.txt' }}" +""" + +RETURN = """ +# Returns the downtime JSON dictionary from the API response under the C(downtime) key. +# See https://docs.datadoghq.com/api/v1/downtimes/#schedule-a-downtime for more details. +downtime: + description: The downtime returned by the API. + type: dict + returned: always + sample: { + "active": true, + "canceled": null, + "creator_id": 1445416, + "disabled": false, + "downtime_type": 2, + "end": null, + "id": 1055751000, + "message": "Downtime for foo:bar", + "monitor_id": null, + "monitor_tags": [ + "foo:bar" + ], + "parent_id": null, + "recurrence": null, + "scope": [ + "test" + ], + "start": 1607015009, + "timezone": "UTC", + "updater_id": null + } +""" + +import traceback + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +# Import Datadog +from ansible.module_utils.common.text.converters import to_native + +DATADOG_IMP_ERR = None +HAS_DATADOG = True +try: + from datadog_api_client.v1 import Configuration, ApiClient, ApiException + from datadog_api_client.v1.api.downtimes_api import DowntimesApi + from datadog_api_client.v1.model.downtime import Downtime + from datadog_api_client.v1.model.downtime_recurrence import DowntimeRecurrence +except ImportError: + DATADOG_IMP_ERR = traceback.format_exc() + HAS_DATADOG = False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + api_key=dict(required=True, no_log=True), + api_host=dict(required=False, default="https://api.datadoghq.com"), + app_key=dict(required=True, no_log=True), + state=dict(required=False, choices=["present", "absent"], default="present"), + monitor_tags=dict(required=False, type="list", elements="str"), + scope=dict(required=False, type="list", elements="str"), + monitor_id=dict(required=False, type="int"), + downtime_message=dict(required=False, no_log=True), + start=dict(required=False, type="int"), + end=dict(required=False, type="int"), + timezone=dict(required=False, type="str"), + rrule=dict(required=False, type="str"), + id=dict(required=False, type="int"), + ) + ) + + # Prepare Datadog + if not HAS_DATADOG: + module.fail_json(msg=missing_required_lib("datadog-api-client"), exception=DATADOG_IMP_ERR) + + configuration = Configuration( + host=module.params["api_host"], + api_key={ + "apiKeyAuth": module.params["api_key"], + "appKeyAuth": module.params["app_key"] + } + ) + with ApiClient(configuration) as api_client: + api_client.user_agent = "ansible_collection/community_general (module_name datadog_downtime) {0}".format( + api_client.user_agent + ) + api_instance = DowntimesApi(api_client) + + # Validate api and app keys + try: + api_instance.list_downtimes(current_only=True) + except ApiException as e: + module.fail_json(msg="Failed to connect Datadog server using given app_key and api_key: {0}".format(e)) + + if module.params["state"] == "present": + schedule_downtime(module, api_client) + elif module.params["state"] == "absent": + cancel_downtime(module, api_client) + + +def _get_downtime(module, api_client): + api = DowntimesApi(api_client) + downtime = None + if module.params["id"]: + try: + downtime = api.get_downtime(module.params["id"]) + except ApiException as e: + module.fail_json(msg="Failed to retrieve downtime with id {0}: {1}".format(module.params["id"], e)) + return downtime + + +def build_downtime(module): + downtime = Downtime() + if module.params["monitor_tags"]: + downtime.monitor_tags = module.params["monitor_tags"] + if module.params["scope"]: + downtime.scope = module.params["scope"] + if module.params["monitor_id"]: + downtime.monitor_id = module.params["monitor_id"] + if module.params["downtime_message"]: + downtime.message = module.params["downtime_message"] + if module.params["start"]: + downtime.start = module.params["start"] + if module.params["end"]: + downtime.end = module.params["end"] + if module.params["timezone"]: + downtime.timezone = module.params["timezone"] + if module.params["rrule"]: + downtime.recurrence = DowntimeRecurrence( + rrule=module.params["rrule"] + ) + return downtime + + +def _post_downtime(module, api_client): + api = DowntimesApi(api_client) + downtime = build_downtime(module) + try: + resp = api.create_downtime(downtime) + module.params["id"] = resp.id + module.exit_json(changed=True, downtime=resp.to_dict()) + except ApiException as e: + module.fail_json(msg="Failed to create downtime: {0}".format(e)) + + +def _equal_dicts(a, b, ignore_keys): + ka = set(a).difference(ignore_keys) + kb = set(b).difference(ignore_keys) + return ka == kb and all(a[k] == b[k] for k in ka) + + +def _update_downtime(module, current_downtime, api_client): + api = DowntimesApi(api_client) + downtime = build_downtime(module) + try: + if current_downtime.disabled: + resp = api.create_downtime(downtime) + else: + resp = api.update_downtime(module.params["id"], downtime) + if _equal_dicts( + resp.to_dict(), + current_downtime.to_dict(), + ["active", "creator_id", "updater_id"] + ): + module.exit_json(changed=False, downtime=resp.to_dict()) + else: + module.exit_json(changed=True, downtime=resp.to_dict()) + except ApiException as e: + module.fail_json(msg="Failed to update downtime: {0}".format(e)) + + +def schedule_downtime(module, api_client): + downtime = _get_downtime(module, api_client) + if downtime is None: + _post_downtime(module, api_client) + else: + _update_downtime(module, downtime, api_client) + + +def cancel_downtime(module, api_client): + downtime = _get_downtime(module, api_client) + api = DowntimesApi(api_client) + if downtime is None: + module.exit_json(changed=False) + try: + api.cancel_downtime(downtime["id"]) + except ApiException as e: + module.fail_json(msg="Failed to create downtime: {0}".format(e)) + + module.exit_json(changed=True) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/monitoring/test_datadog_downtime.py b/tests/unit/plugins/modules/monitoring/test_datadog_downtime.py new file mode 100644 index 0000000000..c7ab6612d7 --- /dev/null +++ b/tests/unit/plugins/modules/monitoring/test_datadog_downtime.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +from ansible_collections.community.general.plugins.modules.monitoring.datadog import datadog_downtime +from ansible_collections.community.general.tests.unit.compat.mock import MagicMock, patch +from ansible_collections.community.general.tests.unit.plugins.modules.utils import ( + AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args +) + +from pytest import importorskip + +# Skip this test if python 2 so datadog_api_client cannot be installed +datadog_api_client = importorskip("datadog_api_client") +Downtime = datadog_api_client.v1.model.downtime.Downtime +DowntimeRecurrence = datadog_api_client.v1.model.downtime_recurrence.DowntimeRecurrence + + +class TestDatadogDowntime(ModuleTestCase): + + def setUp(self): + super(TestDatadogDowntime, self).setUp() + self.module = datadog_downtime + + def tearDown(self): + super(TestDatadogDowntime, self).tearDown() + + def test_without_required_parameters(self): + """Failure must occurs when all parameters are missing""" + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + self.module.main() + + @patch("ansible_collections.community.general.plugins.modules.monitoring.datadog.datadog_downtime.DowntimesApi") + def test_create_downtime_when_no_id(self, downtimes_api_mock): + set_module_args({ + "monitor_tags": ["foo:bar"], + "scope": ["*"], + "monitor_id": 12345, + "downtime_message": "Message", + "start": 1111, + "end": 2222, + "timezone": "UTC", + "rrule": "rrule", + "api_key": "an_api_key", + "app_key": "an_app_key", + }) + + downtime = Downtime() + downtime.monitor_tags = ["foo:bar"] + downtime.scope = ["*"] + downtime.monitor_id = 12345 + downtime.message = "Message" + downtime.start = 1111 + downtime.end = 2222 + downtime.timezone = "UTC" + downtime.recurrence = DowntimeRecurrence( + rrule="rrule" + ) + + create_downtime_mock = MagicMock(return_value=Downtime(id=12345)) + downtimes_api_mock.return_value = MagicMock(create_downtime=create_downtime_mock) + with self.assertRaises(AnsibleExitJson) as result: + self.module.main() + self.assertTrue(result.exception.args[0]['changed']) + self.assertEqual(result.exception.args[0]['downtime']['id'], 12345) + create_downtime_mock.assert_called_once_with(downtime) + + @patch("ansible_collections.community.general.plugins.modules.monitoring.datadog.datadog_downtime.DowntimesApi") + def test_create_downtime_when_id_and_disabled(self, downtimes_api_mock): + set_module_args({ + "id": 1212, + "monitor_tags": ["foo:bar"], + "scope": ["*"], + "monitor_id": 12345, + "downtime_message": "Message", + "start": 1111, + "end": 2222, + "timezone": "UTC", + "rrule": "rrule", + "api_key": "an_api_key", + "app_key": "an_app_key", + }) + + downtime = Downtime() + downtime.monitor_tags = ["foo:bar"] + downtime.scope = ["*"] + downtime.monitor_id = 12345 + downtime.message = "Message" + downtime.start = 1111 + downtime.end = 2222 + downtime.timezone = "UTC" + downtime.recurrence = DowntimeRecurrence( + rrule="rrule" + ) + + create_downtime_mock = MagicMock(return_value=Downtime(id=12345)) + get_downtime_mock = MagicMock(return_value=Downtime(id=1212, disabled=True)) + downtimes_api_mock.return_value = MagicMock( + create_downtime=create_downtime_mock, get_downtime=get_downtime_mock + ) + with self.assertRaises(AnsibleExitJson) as result: + self.module.main() + self.assertTrue(result.exception.args[0]['changed']) + self.assertEqual(result.exception.args[0]['downtime']['id'], 12345) + create_downtime_mock.assert_called_once_with(downtime) + get_downtime_mock.assert_called_once_with(1212) + + @patch("ansible_collections.community.general.plugins.modules.monitoring.datadog.datadog_downtime.DowntimesApi") + def test_update_downtime_when_not_disabled(self, downtimes_api_mock): + set_module_args({ + "id": 1212, + "monitor_tags": ["foo:bar"], + "scope": ["*"], + "monitor_id": 12345, + "downtime_message": "Message", + "start": 1111, + "end": 2222, + "timezone": "UTC", + "rrule": "rrule", + "api_key": "an_api_key", + "app_key": "an_app_key", + }) + + downtime = Downtime() + downtime.monitor_tags = ["foo:bar"] + downtime.scope = ["*"] + downtime.monitor_id = 12345 + downtime.message = "Message" + downtime.start = 1111 + downtime.end = 2222 + downtime.timezone = "UTC" + downtime.recurrence = DowntimeRecurrence( + rrule="rrule" + ) + + update_downtime_mock = MagicMock(return_value=Downtime(id=1212)) + get_downtime_mock = MagicMock(return_value=Downtime(id=1212, disabled=False)) + downtimes_api_mock.return_value = MagicMock( + update_downtime=update_downtime_mock, get_downtime=get_downtime_mock + ) + with self.assertRaises(AnsibleExitJson) as result: + self.module.main() + self.assertTrue(result.exception.args[0]['changed']) + self.assertEqual(result.exception.args[0]['downtime']['id'], 1212) + update_downtime_mock.assert_called_once_with(1212, downtime) + get_downtime_mock.assert_called_once_with(1212) + + @patch("ansible_collections.community.general.plugins.modules.monitoring.datadog.datadog_downtime.DowntimesApi") + def test_update_downtime_no_change(self, downtimes_api_mock): + set_module_args({ + "id": 1212, + "monitor_tags": ["foo:bar"], + "scope": ["*"], + "monitor_id": 12345, + "downtime_message": "Message", + "start": 1111, + "end": 2222, + "timezone": "UTC", + "rrule": "rrule", + "api_key": "an_api_key", + "app_key": "an_app_key", + }) + + downtime = Downtime() + downtime.monitor_tags = ["foo:bar"] + downtime.scope = ["*"] + downtime.monitor_id = 12345 + downtime.message = "Message" + downtime.start = 1111 + downtime.end = 2222 + downtime.timezone = "UTC" + downtime.recurrence = DowntimeRecurrence( + rrule="rrule" + ) + + downtime_get = Downtime() + downtime_get.id = 1212 + downtime_get.disabled = False + downtime_get.monitor_tags = ["foo:bar"] + downtime_get.scope = ["*"] + downtime_get.monitor_id = 12345 + downtime_get.message = "Message" + downtime_get.start = 1111 + downtime_get.end = 2222 + downtime_get.timezone = "UTC" + downtime_get.recurrence = DowntimeRecurrence( + rrule="rrule" + ) + + update_downtime_mock = MagicMock(return_value=downtime_get) + get_downtime_mock = MagicMock(return_value=downtime_get) + downtimes_api_mock.return_value = MagicMock( + update_downtime=update_downtime_mock, get_downtime=get_downtime_mock + ) + with self.assertRaises(AnsibleExitJson) as result: + self.module.main() + self.assertFalse(result.exception.args[0]['changed']) + self.assertEqual(result.exception.args[0]['downtime']['id'], 1212) + update_downtime_mock.assert_called_once_with(1212, downtime) + get_downtime_mock.assert_called_once_with(1212) + + @patch("ansible_collections.community.general.plugins.modules.monitoring.datadog.datadog_downtime.DowntimesApi") + def test_delete_downtime(self, downtimes_api_mock): + set_module_args({ + "id": 1212, + "state": "absent", + "api_key": "an_api_key", + "app_key": "an_app_key", + }) + + cancel_downtime_mock = MagicMock() + get_downtime_mock = MagicMock(return_value=Downtime(id=1212)) + downtimes_api_mock.return_value = MagicMock( + get_downtime=get_downtime_mock, + cancel_downtime=cancel_downtime_mock + ) + with self.assertRaises(AnsibleExitJson) as result: + self.module.main() + self.assertTrue(result.exception.args[0]['changed']) + cancel_downtime_mock.assert_called_once_with(1212) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 4cd5fe4de7..1ce2e74c95 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -21,3 +21,6 @@ openshift ; python_version >= '2.7' # requirement for maven_artifact module lxml semantic_version + +# requirement for datadog_downtime module +datadog-api-client >= 1.0.0b3 ; python_version >= '3.6' \ No newline at end of file