1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Add downtime module (#1437)

* Add downtime module

* review + tests

* importorskip + review

* py36

* make py3.6+ dep explicit
This commit is contained in:
Hippolyte HENRY 2020-12-15 19:50:42 +01:00 committed by GitHub
parent 496be77a2b
commit 19a5975181
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 536 additions and 0 deletions

View file

@ -0,0 +1 @@
./monitoring/datadog/datadog_downtime.py

View file

@ -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()

View file

@ -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)

View file

@ -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'