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

Update pagerduty modules to rest v2 (#42618)

* refactored from procedural to OOP

* updated ongoing maintenance windows to PagerDuty REST API v2

* update create maintenance windows to PagerDuty REST API v2

* update absent maintenance windows to PagerDuty REST API v2

* update pager alert module to PagerDuty REST API v2

* removed basic HTTP authorization
updated parameter description and examples

* fix failed sanity checks

* revised documentation according to review

* make obsolete service key parameter an alias to a new integration key parameter
This commit is contained in:
Alex Dukhno 2018-07-31 17:39:29 +03:00 committed by Ryan Brown
parent 122780abce
commit dd28be3aab
4 changed files with 298 additions and 117 deletions

View file

@ -35,31 +35,27 @@ options:
choices: [ "running", "started", "ongoing", "absent" ] choices: [ "running", "started", "ongoing", "absent" ]
name: name:
description: description:
- PagerDuty unique subdomain. - PagerDuty unique subdomain. Obsolete. It is not used with PagerDuty REST v2 API.
required: true
user: user:
description: description:
- PagerDuty user ID. - PagerDuty user ID. Obsolete. Please, use I(token) for authorization.
required: true
passwd:
description:
- PagerDuty user password.
required: true
token: token:
description: description:
- A pagerduty token, generated on the pagerduty site. Can be used instead of - A pagerduty token, generated on the pagerduty site. It is used for authorization.
user/passwd combination.
required: true required: true
version_added: '1.8' version_added: '1.8'
requester_id: requester_id:
description: description:
- ID of user making the request. Only needed when using a token and creating a maintenance_window. - ID of user making the request. Only needed when creating a maintenance_window.
required: true
version_added: '1.8' version_added: '1.8'
service: service:
description: description:
- A comma separated list of PagerDuty service IDs. - A comma separated list of PagerDuty service IDs.
aliases: [ services ] aliases: [ services ]
window_id:
description:
- ID of maintenance window. Only needed when absent a maintenance_window.
version_added: "2.7"
hours: hours:
description: description:
- Length of maintenance window in hours. - Length of maintenance window in hours.
@ -83,28 +79,21 @@ options:
''' '''
EXAMPLES = ''' 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 # List ongoing maintenance windows using a token
- pagerduty: - pagerduty:
name: companyabc name: companyabc
token: xxxxxxxxxxxxxx token: xxxxxxxxxxxxxx
state: ongoing 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: - pagerduty:
name: companyabc name: companyabc
user: example@example.com user: example@example.com
passwd: password123 token: yourtoken
state: running state: running
service: FOO123 service: FOO123
# Create a 5 minute maintenance window for service FOO123, using a token # Create a 5 minute maintenance window for service FOO123
- pagerduty: - pagerduty:
name: companyabc name: companyabc
token: xxxxxxxxxxxxxx token: xxxxxxxxxxxxxx
@ -118,7 +107,6 @@ EXAMPLES = '''
- pagerduty: - pagerduty:
name: companyabc name: companyabc
user: example@example.com user: example@example.com
passwd: password123
state: running state: running
service: FOO123 service: FOO123
hours: 4 hours: 4
@ -129,9 +117,8 @@ EXAMPLES = '''
- pagerduty: - pagerduty:
name: companyabc name: companyabc
user: example@example.com user: example@example.com
passwd: password123
state: absent state: absent
service: '{{ pd_window.result.maintenance_window.id }}' window_id: '{{ pd_window.result.maintenance_window.id }}'
''' '''
import datetime import datetime
@ -143,87 +130,86 @@ from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_bytes from ansible.module_utils._text import to_bytes
def auth_header(user, passwd, token): class PagerDutyRequest(object):
if token: def __init__(self, module, name, user, token):
return "Token token=%s" % 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', '')) def ongoing(self, http_call=fetch_url):
return "Basic %s" % auth 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): json_out = self._read_response(response)
url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows/ongoing"
headers = {"Authorization": auth_header(user, passwd, token)}
response, info = fetch_url(module, url, headers=headers) return False, json_out, False
if info['status'] != 200:
module.fail_json(msg="failed to lookup the ongoing window: %s" % info['msg'])
try: def create(self, requester_id, service, hours, minutes, desc, http_call=fetch_url):
json_out = json.loads(response.read()) if not requester_id:
except: self.module.fail_json(msg="requester_id is required when maintenance window should be created")
json_out = ""
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): start, end = self._compute_start_end_time(hours, minutes)
now = datetime.datetime.utcnow() services = self._create_services_payload(service)
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")
url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows" request_data = {'maintenance_window': {'start_time': start, 'end_time': end, 'description': desc, 'services': services}}
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}}
if requester_id: data = json.dumps(request_data)
request_data['requester_id'] = requester_id response, info = http_call(self.module, url, data=data, headers=headers, method='POST')
else: if info['status'] != 201:
if token: self.module.fail_json(msg="failed to create the window: %s" % info['msg'])
module.fail_json(msg="requester_id is required when using a token")
data = json.dumps(request_data) json_out = self._read_response(response)
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'])
try: return False, json_out, True
json_out = json.loads(response.read())
except:
json_out = ""
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): def absent(self, window_id, http_call=fetch_url):
url = "https://" + name + ".pagerduty.com/api/v1/maintenance_windows/" + service[0] url = "https://api.pagerduty.com/maintenance_windows/" + window_id
headers = { headers = dict(self.headers)
'Authorization': auth_header(user, passwd, token),
'Content-Type': 'application/json',
}
request_data = {}
if requester_id: response, info = http_call(self.module, url, headers=headers, method='DELETE')
request_data['requester_id'] = requester_id if info['status'] != 204:
else: self.module.fail_json(msg="failed to delete the window: %s" % info['msg'])
if token:
module.fail_json(msg="requester_id is required when using a token")
data = json.dumps(request_data) json_out = self._read_response(response)
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'])
try: return False, json_out, True
json_out = json.loads(response.read())
except:
json_out = ""
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(): def main():
@ -231,11 +217,11 @@ def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
state=dict(required=True, choices=['running', 'started', 'ongoing', 'absent']), state=dict(required=True, choices=['running', 'started', 'ongoing', 'absent']),
name=dict(required=True), name=dict(required=False),
user=dict(required=False), user=dict(required=False),
passwd=dict(required=False, no_log=True), token=dict(required=True, no_log=True),
token=dict(required=False, no_log=True),
service=dict(required=False, type='list', aliases=["services"]), service=dict(required=False, type='list', aliases=["services"]),
window_id=dict(required=False),
requester_id=dict(required=False), requester_id=dict(required=False),
hours=dict(default='1', required=False), hours=dict(default='1', required=False),
minutes=dict(default='0', required=False), minutes=dict(default='0', required=False),
@ -247,30 +233,28 @@ def main():
state = module.params['state'] state = module.params['state']
name = module.params['name'] name = module.params['name']
user = module.params['user'] user = module.params['user']
passwd = module.params['passwd']
token = module.params['token']
service = module.params['service'] service = module.params['service']
window_id = module.params['window_id']
hours = module.params['hours'] hours = module.params['hours']
minutes = module.params['minutes'] minutes = module.params['minutes']
token = module.params['token'] token = module.params['token']
desc = module.params['desc'] desc = module.params['desc']
requester_id = module.params['requester_id'] requester_id = module.params['requester_id']
if not token and not (user or passwd): pd = PagerDutyRequest(module, name, user, token)
module.fail_json(msg="neither user and passwd nor token specified")
if state == "running" or state == "started": if state == "running" or state == "started":
if not service: if not service:
module.fail_json(msg="service not specified") 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: if rc == 0:
changed = True changed = True
if state == "ongoing": if state == "ongoing":
(rc, out, changed) = ongoing(module, name, user, passwd, token) (rc, out, changed) = pd.ongoing()
if state == "absent": 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: if rc != 0:
module.fail_json(msg="failed", result=out) module.fail_json(msg="failed", result=out)

View file

@ -26,13 +26,21 @@ requirements:
options: options:
name: name:
description: 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 required: true
version_added: "2.7"
service_key: service_key:
description:
- The GUID of one of your "Generic API" services. Obsolete. Please use I(integration_key).
integration_key:
description: description:
- The GUID of one of your "Generic API" services. - 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 required: true
version_added: "2.7"
state: state:
description: description:
- Type of event to be sent. - 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 - 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. trigger event. Acknowledge events referencing resolved or nonexistent incidents will be discarded.
required: false required: false
version_added: "2.7"
client: client:
description: description:
- The name of the monitoring client that is triggering this event. - The name of the monitoring client that is triggering this event.
@ -76,15 +85,17 @@ EXAMPLES = '''
# Trigger an incident with just the basic options # Trigger an incident with just the basic options
- pagerduty_alert: - pagerduty_alert:
name: companyabc name: companyabc
service_key: xxx integration_key: xxx
api_key: yourapikey api_key: yourapikey
service_id: PDservice
state: triggered state: triggered
desc: problem that led to this trigger desc: problem that led to this trigger
# Trigger an incident with more options # Trigger an incident with more options
- pagerduty_alert: - pagerduty_alert:
service_key: xxx integration_key: xxx
api_key: yourapikey api_key: yourapikey
service_id: PDservice
state: triggered state: triggered
desc: problem that led to this trigger desc: problem that led to this trigger
incident_key: somekey incident_key: somekey
@ -93,16 +104,18 @@ EXAMPLES = '''
# Acknowledge an incident based on incident_key # Acknowledge an incident based on incident_key
- pagerduty_alert: - pagerduty_alert:
service_key: xxx integration_key: xxx
api_key: yourapikey api_key: yourapikey
service_id: PDservice
state: acknowledged state: acknowledged
incident_key: somekey incident_key: somekey
desc: "some text for incident's log" desc: "some text for incident's log"
# Resolve an incident based on incident_key # Resolve an incident based on incident_key
- pagerduty_alert: - pagerduty_alert:
service_key: xxx integration_key: xxx
api_key: yourapikey api_key: yourapikey
service_id: PDservice
state: resolved state: resolved
incident_key: somekey incident_key: somekey
desc: "some text for incident's log" desc: "some text for incident's log"
@ -111,23 +124,31 @@ import json
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url 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): def check(module, name, state, service_id, integration_key, api_key, incident_key=None, http_call=fetch_url):
url = "https://%s.pagerduty.com/api/v1/incidents" % name url = 'https://api.pagerduty.com/incidents'
headers = { headers = {
"Content-type": "application/json", "Content-type": "application/json",
"Authorization": "Token token=%s" % api_key "Authorization": "Token token=%s" % api_key,
'Accept': 'application/vnd.pagerduty+json;version=2'
} }
data = { params = {
"service_key": service_key, 'service_ids[]': service_id,
"incident_key": incident_key, 'sort_by': 'incident_number:desc',
"sort_by": "incident_number:desc" 'time_zone': 'UTC'
} }
if incident_key:
params['incident_key'] = incident_key
response, info = fetch_url(module, url, method='get', url_parts = list(urlparse(url))
headers=headers, data=json.dumps(data)) url_parts[4] = urlencode(params, True)
url = urlunparse(url_parts)
response, info = http_call(module, url, method='get', headers=headers)
if info['status'] != 200: if info['status'] != 200:
module.fail_json(msg="failed to check current incident status." 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(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
name=dict(required=True), name=dict(required=False),
service_key=dict(required=True), service_id=dict(required=True),
service_key=dict(require=False),
integration_key=dict(require=False),
api_key=dict(required=True), api_key=dict(required=True),
state=dict(required=True, state=dict(required=True,
choices=['triggered', 'acknowledged', 'resolved']), choices=['triggered', 'acknowledged', 'resolved']),
@ -181,6 +204,8 @@ def main():
) )
name = module.params['name'] name = module.params['name']
service_id = module.params['service_id']
integration_key = module.params['integration_key']
service_key = module.params['service_key'] service_key = module.params['service_key']
api_key = module.params['api_key'] api_key = module.params['api_key']
state = module.params['state'] state = module.params['state']
@ -189,6 +214,14 @@ def main():
desc = module.params['desc'] desc = module.params['desc']
incident_key = module.params['incident_key'] 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 = { state_event_dict = {
'triggered': 'trigger', 'triggered': 'trigger',
'acknowledged': 'acknowledge', 'acknowledged': 'acknowledge',
@ -201,11 +234,11 @@ def main():
module.fail_json(msg="incident_key is required for " module.fail_json(msg="incident_key is required for "
"acknowledge or resolve events") "acknowledge or resolve events")
out, changed = check(module, name, state, out, changed = check(module, name, state, service_id,
service_key, api_key, incident_key) integration_key, api_key, incident_key)
if not module.check_mode and changed is True: 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) incident_key, client, client_url)
module.exit_json(result=out, changed=changed) module.exit_json(result=out, changed=changed)

View file

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

View file

@ -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"}]}'