diff --git a/lib/ansible/modules/monitoring/pagerduty.py b/lib/ansible/modules/monitoring/pagerduty.py index 7067f7fac7..27c7811a6a 100644 --- a/lib/ansible/modules/monitoring/pagerduty.py +++ b/lib/ansible/modules/monitoring/pagerduty.py @@ -35,31 +35,27 @@ options: choices: [ "running", "started", "ongoing", "absent" ] name: description: - - PagerDuty unique subdomain. - required: true + - PagerDuty unique subdomain. Obsolete. It is not used with PagerDuty REST v2 API. user: description: - - PagerDuty user ID. - required: true - passwd: - description: - - PagerDuty user password. - required: true + - PagerDuty user ID. Obsolete. Please, use I(token) for authorization. token: description: - - A pagerduty token, generated on the pagerduty site. Can be used instead of - user/passwd combination. + - A pagerduty token, generated on the pagerduty site. It is used for authorization. required: true version_added: '1.8' requester_id: description: - - ID of user making the request. Only needed when using a token and creating a maintenance_window. - required: true + - ID of user making the request. Only needed when creating a maintenance_window. version_added: '1.8' service: description: - A comma separated list of PagerDuty service IDs. aliases: [ services ] + window_id: + description: + - ID of maintenance window. Only needed when absent a maintenance_window. + version_added: "2.7" hours: description: - Length of maintenance window in hours. @@ -83,28 +79,21 @@ options: ''' EXAMPLES = ''' -# List ongoing maintenance windows using a user/passwd -- pagerduty: - name: companyabc - user: example@example.com - passwd: password123 - state: ongoing - # List ongoing maintenance windows using a token - pagerduty: name: companyabc token: xxxxxxxxxxxxxx state: ongoing -# Create a 1 hour maintenance window for service FOO123, using a user/passwd +# Create a 1 hour maintenance window for service FOO123 - pagerduty: name: companyabc user: example@example.com - passwd: password123 + token: yourtoken state: running service: FOO123 -# Create a 5 minute maintenance window for service FOO123, using a token +# Create a 5 minute maintenance window for service FOO123 - pagerduty: name: companyabc token: xxxxxxxxxxxxxx @@ -118,7 +107,6 @@ EXAMPLES = ''' - pagerduty: name: companyabc user: example@example.com - passwd: password123 state: running service: FOO123 hours: 4 @@ -129,9 +117,8 @@ EXAMPLES = ''' - pagerduty: name: companyabc user: example@example.com - passwd: password123 state: absent - service: '{{ pd_window.result.maintenance_window.id }}' + window_id: '{{ pd_window.result.maintenance_window.id }}' ''' import datetime @@ -143,87 +130,86 @@ from ansible.module_utils.urls import fetch_url from ansible.module_utils._text import to_bytes -def auth_header(user, passwd, token): - if token: - return "Token token=%s" % token +class PagerDutyRequest(object): + def __init__(self, module, name, user, token): + self.module = module + self.name = name + self.user = user + self.token = token + self.headers = { + 'Content-Type': 'application/json', + "Authorization": self._auth_header(), + 'Accept': 'application/vnd.pagerduty+json;version=2' + } - auth = base64.b64encode(to_bytes('%s:%s' % (user, passwd)).replace('\n', '')) - return "Basic %s" % auth + def ongoing(self, http_call=fetch_url): + url = "https://api.pagerduty.com/maintenance_windows?filter=ongoing" + headers = dict(self.headers) + response, info = http_call(self.module, url, headers=headers) + if info['status'] != 200: + self.module.fail_json(msg="failed to lookup the ongoing window: %s" % info['msg']) -def ongoing(module, name, user, passwd, token): - url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows/ongoing" - headers = {"Authorization": auth_header(user, passwd, token)} + json_out = self._read_response(response) - response, info = fetch_url(module, url, headers=headers) - if info['status'] != 200: - module.fail_json(msg="failed to lookup the ongoing window: %s" % info['msg']) + return False, json_out, False - try: - json_out = json.loads(response.read()) - except: - json_out = "" + def create(self, requester_id, service, hours, minutes, desc, http_call=fetch_url): + if not requester_id: + self.module.fail_json(msg="requester_id is required when maintenance window should be created") - return False, json_out, False + url = 'https://api.pagerduty.com/maintenance_windows' + headers = dict(self.headers) + headers.update({'From': requester_id}) -def create(module, name, user, passwd, token, requester_id, service, hours, minutes, desc): - now = datetime.datetime.utcnow() - later = now + datetime.timedelta(hours=int(hours), minutes=int(minutes)) - start = now.strftime("%Y-%m-%dT%H:%M:%SZ") - end = later.strftime("%Y-%m-%dT%H:%M:%SZ") + start, end = self._compute_start_end_time(hours, minutes) + services = self._create_services_payload(service) - url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows" - headers = { - 'Authorization': auth_header(user, passwd, token), - 'Content-Type': 'application/json', - } - request_data = {'maintenance_window': {'start_time': start, 'end_time': end, 'description': desc, 'service_ids': service}} + request_data = {'maintenance_window': {'start_time': start, 'end_time': end, 'description': desc, 'services': services}} - if requester_id: - request_data['requester_id'] = requester_id - else: - if token: - module.fail_json(msg="requester_id is required when using a token") + data = json.dumps(request_data) + response, info = http_call(self.module, url, data=data, headers=headers, method='POST') + if info['status'] != 201: + self.module.fail_json(msg="failed to create the window: %s" % info['msg']) - data = json.dumps(request_data) - response, info = fetch_url(module, url, data=data, headers=headers, method='POST') - if info['status'] != 201: - module.fail_json(msg="failed to create the window: %s" % info['msg']) + json_out = self._read_response(response) - try: - json_out = json.loads(response.read()) - except: - json_out = "" + return False, json_out, True - return False, json_out, True + def _create_services_payload(self, service): + if (isinstance(service, list)): + return [{'id': s, 'type': 'service_reference'} for s in service] + else: + return [{'id': service, 'type': 'service_reference'}] + def _compute_start_end_time(self, hours, minutes): + now = datetime.datetime.utcnow() + later = now + datetime.timedelta(hours=int(hours), minutes=int(minutes)) + start = now.strftime("%Y-%m-%dT%H:%M:%SZ") + end = later.strftime("%Y-%m-%dT%H:%M:%SZ") + return start, end -def absent(module, name, user, passwd, token, requester_id, service): - url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows/" + service[0] - headers = { - 'Authorization': auth_header(user, passwd, token), - 'Content-Type': 'application/json', - } - request_data = {} + def absent(self, window_id, http_call=fetch_url): + url = "https://api.pagerduty.com/maintenance_windows/" + window_id + headers = dict(self.headers) - if requester_id: - request_data['requester_id'] = requester_id - else: - if token: - module.fail_json(msg="requester_id is required when using a token") + response, info = http_call(self.module, url, headers=headers, method='DELETE') + if info['status'] != 204: + self.module.fail_json(msg="failed to delete the window: %s" % info['msg']) - data = json.dumps(request_data) - response, info = fetch_url(module, url, data=data, headers=headers, method='DELETE') - if info['status'] != 204: - module.fail_json(msg="failed to delete the window: %s" % info['msg']) + json_out = self._read_response(response) - try: - json_out = json.loads(response.read()) - except: - json_out = "" + return False, json_out, True - return False, json_out, True + def _auth_header(self): + return "Token token=%s" % self.token + + def _read_response(self, response): + try: + return json.loads(response.read()) + except: + return "" def main(): @@ -231,11 +217,11 @@ def main(): module = AnsibleModule( argument_spec=dict( state=dict(required=True, choices=['running', 'started', 'ongoing', 'absent']), - name=dict(required=True), + name=dict(required=False), user=dict(required=False), - passwd=dict(required=False, no_log=True), - token=dict(required=False, no_log=True), + token=dict(required=True, no_log=True), service=dict(required=False, type='list', aliases=["services"]), + window_id=dict(required=False), requester_id=dict(required=False), hours=dict(default='1', required=False), minutes=dict(default='0', required=False), @@ -247,30 +233,28 @@ def main(): state = module.params['state'] name = module.params['name'] user = module.params['user'] - passwd = module.params['passwd'] - token = module.params['token'] service = module.params['service'] + window_id = module.params['window_id'] hours = module.params['hours'] minutes = module.params['minutes'] token = module.params['token'] desc = module.params['desc'] requester_id = module.params['requester_id'] - if not token and not (user or passwd): - module.fail_json(msg="neither user and passwd nor token specified") + pd = PagerDutyRequest(module, name, user, token) if state == "running" or state == "started": if not service: module.fail_json(msg="service not specified") - (rc, out, changed) = create(module, name, user, passwd, token, requester_id, service, hours, minutes, desc) + (rc, out, changed) = pd.create(requester_id, service, hours, minutes, desc) if rc == 0: changed = True if state == "ongoing": - (rc, out, changed) = ongoing(module, name, user, passwd, token) + (rc, out, changed) = pd.ongoing() if state == "absent": - (rc, out, changed) = absent(module, name, user, passwd, token, requester_id, service) + (rc, out, changed) = pd.absent(window_id) if rc != 0: module.fail_json(msg="failed", result=out) diff --git a/lib/ansible/modules/monitoring/pagerduty_alert.py b/lib/ansible/modules/monitoring/pagerduty_alert.py index 7e991b8e7f..f9ea471dc3 100644 --- a/lib/ansible/modules/monitoring/pagerduty_alert.py +++ b/lib/ansible/modules/monitoring/pagerduty_alert.py @@ -26,13 +26,21 @@ requirements: options: name: description: - - PagerDuty unique subdomain. + - PagerDuty unique subdomain. Obsolete. It is not used with PagerDuty REST v2 API. + service_id: + description: + - ID of PagerDuty service when incidents will be triggered, acknowledged or resolved. required: true + version_added: "2.7" service_key: + description: + - The GUID of one of your "Generic API" services. Obsolete. Please use I(integration_key). + integration_key: description: - The GUID of one of your "Generic API" services. - - This is the "service key" listed on a Generic API's service detail page. + - This is the "integration key" listed on a "Integrations" tab of PagerDuty service. required: true + version_added: "2.7" state: description: - Type of event to be sent. @@ -62,6 +70,7 @@ options: - For C(acknowledged) or C(resolved) I(state) - This should be the incident_key you received back when the incident was first opened by a trigger event. Acknowledge events referencing resolved or nonexistent incidents will be discarded. required: false + version_added: "2.7" client: description: - The name of the monitoring client that is triggering this event. @@ -76,15 +85,17 @@ EXAMPLES = ''' # Trigger an incident with just the basic options - pagerduty_alert: name: companyabc - service_key: xxx + integration_key: xxx api_key: yourapikey + service_id: PDservice state: triggered desc: problem that led to this trigger # Trigger an incident with more options - pagerduty_alert: - service_key: xxx + integration_key: xxx api_key: yourapikey + service_id: PDservice state: triggered desc: problem that led to this trigger incident_key: somekey @@ -93,16 +104,18 @@ EXAMPLES = ''' # Acknowledge an incident based on incident_key - pagerduty_alert: - service_key: xxx + integration_key: xxx api_key: yourapikey + service_id: PDservice state: acknowledged incident_key: somekey desc: "some text for incident's log" # Resolve an incident based on incident_key - pagerduty_alert: - service_key: xxx + integration_key: xxx api_key: yourapikey + service_id: PDservice state: resolved incident_key: somekey desc: "some text for incident's log" @@ -111,23 +124,31 @@ import json from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url +from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode, urlunparse -def check(module, name, state, service_key, api_key, incident_key=None): - url = "https://%s.pagerduty.com/api/v1/incidents" % name +def check(module, name, state, service_id, integration_key, api_key, incident_key=None, http_call=fetch_url): + url = 'https://api.pagerduty.com/incidents' headers = { "Content-type": "application/json", - "Authorization": "Token token=%s" % api_key + "Authorization": "Token token=%s" % api_key, + 'Accept': 'application/vnd.pagerduty+json;version=2' } - data = { - "service_key": service_key, - "incident_key": incident_key, - "sort_by": "incident_number:desc" + params = { + 'service_ids[]': service_id, + 'sort_by': 'incident_number:desc', + 'time_zone': 'UTC' } + if incident_key: + params['incident_key'] = incident_key - response, info = fetch_url(module, url, method='get', - headers=headers, data=json.dumps(data)) + url_parts = list(urlparse(url)) + url_parts[4] = urlencode(params, True) + + url = urlunparse(url_parts) + + response, info = http_call(module, url, method='get', headers=headers) if info['status'] != 200: module.fail_json(msg="failed to check current incident status." @@ -167,8 +188,10 @@ def send_event(module, service_key, event_type, desc, def main(): module = AnsibleModule( argument_spec=dict( - name=dict(required=True), - service_key=dict(required=True), + name=dict(required=False), + service_id=dict(required=True), + service_key=dict(require=False), + integration_key=dict(require=False), api_key=dict(required=True), state=dict(required=True, choices=['triggered', 'acknowledged', 'resolved']), @@ -181,6 +204,8 @@ def main(): ) name = module.params['name'] + service_id = module.params['service_id'] + integration_key = module.params['integration_key'] service_key = module.params['service_key'] api_key = module.params['api_key'] state = module.params['state'] @@ -189,6 +214,14 @@ def main(): desc = module.params['desc'] incident_key = module.params['incident_key'] + if integration_key is None: + if service_key is not None: + integration_key = service_key + module.warn('"service_key" is obsolete parameter and will be removed.' + ' Please, use "integration_key" instead') + else: + module.fail_json(msg="'integration_key' is required parameter") + state_event_dict = { 'triggered': 'trigger', 'acknowledged': 'acknowledge', @@ -201,11 +234,11 @@ def main(): module.fail_json(msg="incident_key is required for " "acknowledge or resolve events") - out, changed = check(module, name, state, - service_key, api_key, incident_key) + out, changed = check(module, name, state, service_id, + integration_key, api_key, incident_key) if not module.check_mode and changed is True: - out = send_event(module, service_key, event_type, desc, + out = send_event(module, integration_key, event_type, desc, incident_key, client, client_url) module.exit_json(result=out, changed=changed) diff --git a/test/units/modules/monitoring/test_pagerduty.py b/test/units/modules/monitoring/test_pagerduty.py new file mode 100644 index 0000000000..ceeaac6d98 --- /dev/null +++ b/test/units/modules/monitoring/test_pagerduty.py @@ -0,0 +1,123 @@ +from ansible.compat.tests import unittest +from ansible.modules.monitoring import pagerduty + +import json + + +class PagerDutyTest(unittest.TestCase): + def setUp(self): + self.pd = pagerduty.PagerDutyRequest(module=pagerduty, name='name', user='user', token='token') + + def _assert_ongoing_maintenance_windows(self, module, url, headers): + self.assertEquals('https://api.pagerduty.com/maintenance_windows?filter=ongoing', url) + return object(), {'status': 200} + + def _assert_ongoing_window_with_v1_compatible_header(self, module, url, headers, data=None, method=None): + self.assertDictContainsSubset( + {'Accept': 'application/vnd.pagerduty+json;version=2'}, + headers, + 'Accept:application/vnd.pagerduty+json;version=2 HTTP header not found' + ) + return object(), {'status': 200} + + def _assert_create_a_maintenance_window_url(self, module, url, headers, data=None, method=None): + self.assertEquals('https://api.pagerduty.com/maintenance_windows', url) + return object(), {'status': 201} + + def _assert_create_a_maintenance_window_http_method(self, module, url, headers, data=None, method=None): + self.assertEquals('POST', method) + return object(), {'status': 201} + + def _assert_create_a_maintenance_window_from_header(self, module, url, headers, data=None, method=None): + self.assertDictContainsSubset( + {'From': 'requester_id'}, + headers, + 'From:requester_id HTTP header not found' + ) + return object(), {'status': 201} + + def _assert_create_window_with_v1_compatible_header(self, module, url, headers, data=None, method=None): + self.assertDictContainsSubset( + {'Accept': 'application/vnd.pagerduty+json;version=2'}, + headers, + 'Accept:application/vnd.pagerduty+json;version=2 HTTP header not found' + ) + return object(), {'status': 201} + + def _assert_create_window_payload(self, module, url, headers, data=None, method=None): + payload = json.loads(data) + window_data = payload['maintenance_window'] + self.assertTrue('start_time' in window_data, '"start_time" is requiered attribute') + self.assertTrue('end_time' in window_data, '"end_time" is requiered attribute') + self.assertTrue('services' in window_data, '"services" is requiered attribute') + return object(), {'status': 201} + + def _assert_create_window_single_service(self, module, url, headers, data=None, method=None): + payload = json.loads(data) + window_data = payload['maintenance_window'] + services = window_data['services'] + self.assertEquals( + [{'id': 'service_id', 'type': 'service_reference'}], + services + ) + return object(), {'status': 201} + + def _assert_create_window_multiple_service(self, module, url, headers, data=None, method=None): + payload = json.loads(data) + window_data = payload['maintenance_window'] + services = window_data['services'] + print(services) + self.assertEquals( + [ + {'id': 'service_id_1', 'type': 'service_reference'}, + {'id': 'service_id_2', 'type': 'service_reference'}, + {'id': 'service_id_3', 'type': 'service_reference'}, + ], + services + ) + return object(), {'status': 201} + + def _assert_absent_maintenance_window_url(self, module, url, headers, method=None): + self.assertEquals('https://api.pagerduty.com/maintenance_windows/window_id', url) + return object(), {'status': 204} + + def _assert_absent_window_with_v1_compatible_header(self, module, url, headers, method=None): + self.assertDictContainsSubset( + {'Accept': 'application/vnd.pagerduty+json;version=2'}, + headers, + 'Accept:application/vnd.pagerduty+json;version=2 HTTP header not found' + ) + return object(), {'status': 204} + + def test_ongoing_maintenance_windos_url(self): + self.pd.ongoing(http_call=self._assert_ongoing_maintenance_windows) + + def test_ongoing_maintenance_windos_compatibility_header(self): + self.pd.ongoing(http_call=self._assert_ongoing_window_with_v1_compatible_header) + + def test_create_maintenance_window_url(self): + self.pd.create('requester_id', 'service', 1, 0, 'desc', http_call=self._assert_create_a_maintenance_window_url) + + def test_create_maintenance_window_http_method(self): + self.pd.create('requester_id', 'service', 1, 0, 'desc', http_call=self._assert_create_a_maintenance_window_http_method) + + def test_create_maintenance_from_header(self): + self.pd.create('requester_id', 'service', 1, 0, 'desc', http_call=self._assert_create_a_maintenance_window_from_header) + + def test_create_maintenance_compatibility_header(self): + self.pd.create('requester_id', 'service', 1, 0, 'desc', http_call=self._assert_create_window_with_v1_compatible_header) + + def test_create_maintenance_request_payload(self): + self.pd.create('requester_id', 'service', 1, 0, 'desc', http_call=self._assert_create_window_payload) + + def test_create_maintenance_for_single_service(self): + self.pd.create('requester_id', 'service_id', 1, 0, 'desc', http_call=self._assert_create_window_single_service) + + def test_create_maintenance_for_multiple_services(self): + self.pd.create('requester_id', ['service_id_1', 'service_id_2', 'service_id_3'], 1, 0, 'desc', http_call=self._assert_create_window_multiple_service) + + def test_absent_maintenance_window_url(self): + self.pd.absent('window_id', http_call=self._assert_absent_maintenance_window_url) + + def test_absent_maintenance_compatibility_header(self): + self.pd.absent('window_id', http_call=self._assert_absent_window_with_v1_compatible_header) diff --git a/test/units/modules/monitoring/test_pagerduty_alert.py b/test/units/modules/monitoring/test_pagerduty_alert.py new file mode 100644 index 0000000000..d992462f61 --- /dev/null +++ b/test/units/modules/monitoring/test_pagerduty_alert.py @@ -0,0 +1,41 @@ +from ansible.compat.tests import unittest +from ansible.modules.monitoring import pagerduty_alert + +from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode, urlunparse + + +class PagerDutyAlertsTest(unittest.TestCase): + def _assert_incident_api(self, module, url, method, headers): + self.assertTrue('https://api.pagerduty.com/incidents' in url, 'url must contain REST API v2 network path') + self.assertTrue('service_ids%5B%5D=service_id' in url, 'url must contain service id to filter incidents') + self.assertTrue('sort_by=incident_number%3Adesc' in url, 'url should contain sorting parameter') + self.assertTrue('time_zone=UTC' in url, 'url should contain time zone parameter') + return Response(), {'status': 200} + + def _assert_compatibility_header(self, module, url, method, headers): + self.assertDictContainsSubset( + {'Accept': 'application/vnd.pagerduty+json;version=2'}, + headers, + 'Accept:application/vnd.pagerduty+json;version=2 HTTP header not found' + ) + return Response(), {'status': 200} + + def _assert_incident_key(self, module, url, method, headers): + self.assertTrue('incident_key=incident_key_value' in url, 'url must contain incident key') + return Response(), {'status': 200} + + def test_incident_url(self): + pagerduty_alert.check(None, 'name', 'state', 'service_id', 'integration_key', 'api_key', http_call=self._assert_incident_api) + + def test_compatibility_header(self): + pagerduty_alert.check(None, 'name', 'state', 'service_id', 'integration_key', 'api_key', http_call=self._assert_compatibility_header) + + def test_incident_key_in_url_when_it_is_given(self): + pagerduty_alert.check( + None, 'name', 'state', 'service_id', 'integration_key', 'api_key', incident_key='incident_key_value', http_call=self._assert_incident_key + ) + + +class Response(object): + def read(self): + return '{"incidents":[{"id": "incident_id", "status": "triggered"}]}'