"""
Module for manageing PagerDuty resource
:configuration: This module can be used by specifying the name of a
configuration profile in the minion config, minion pillar, or master
config. The default configuration profile name is 'pagerduty.'
For example:
.. code-block:: yaml
pagerduty:
pagerduty.api_key: F3Rbyjbve43rfFWf2214
pagerduty.subdomain: mysubdomain
For PagerDuty API details, see https://developer.pagerduty.com/documentation/rest
"""
import requests
import salt.utils.json
[docs]
def __virtual__():
"""
No dependencies outside of what Salt itself requires
"""
return True
[docs]
def get_users(profile="pagerduty", subdomain=None, api_key=None):
"""
List users belonging to this account
CLI Example:
.. code-block:: bash
salt myminion pagerduty.get_users
"""
return _list_items(
"users",
"id",
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
[docs]
def get_services(profile="pagerduty", subdomain=None, api_key=None):
"""
List services belonging to this account
CLI Example:
.. code-block:: bash
salt myminion pagerduty.get_services
"""
return _list_items(
"services",
"id",
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
[docs]
def get_schedules(profile="pagerduty", subdomain=None, api_key=None):
"""
List schedules belonging to this account
CLI Example:
.. code-block:: bash
salt myminion pagerduty.get_schedules
"""
return _list_items(
"schedules",
"id",
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
[docs]
def get_escalation_policies(profile="pagerduty", subdomain=None, api_key=None):
"""
List escalation_policies belonging to this account
CLI Example:
.. code-block:: bash
salt myminion pagerduty.get_escalation_policies
"""
return _list_items(
"escalation_policies",
"id",
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
def _list_items(action, key, profile=None, subdomain=None, api_key=None):
"""
List items belonging to an API call.
This method should be in utils.pagerduty.
"""
items = _query(profile=profile, subdomain=subdomain, api_key=api_key, action=action)
ret = {}
for item in items[action]:
ret[item[key]] = item
return ret
def _query(
method="GET",
profile=None,
url=None,
path="api/v1",
action=None,
api_key=None,
params=None,
data=None,
subdomain=None,
verify_ssl=True,
):
"""
Query the PagerDuty API.
This method should be in utils.pagerduty.
"""
if profile:
creds = __salt__["config.option"](profile)
else:
creds = {
"pagerduty.api_key": api_key,
"pagerduty.subdomain": subdomain,
}
if url is None:
url = f"https://{creds['pagerduty.subdomain']}.pagerduty.com/{path}/{action}"
if params is None:
params = {}
if data is None:
data = {}
headers = {"Authorization": f"Token token={creds['pagerduty.api_key']}"}
if method != "GET":
headers["Content-type"] = "application/json"
# FIXME
# pylint: disable=missing-timeout
result = requests.request(
method,
url,
headers=headers,
params=params,
data=salt.utils.json.dumps(data),
verify=verify_ssl,
)
if result.text is None or result.text == "":
return None
result_json = result.json()
# if this query supports pagination, loop and fetch all results, merge them together
if "total" in result_json and "offset" in result_json and "limit" in result_json:
offset = result_json["offset"]
limit = result_json["limit"]
total = result_json["total"]
while offset + limit < total:
offset = offset + limit
limit = 100
data["offset"] = offset
data["limit"] = limit
# FIXME
# pylint: disable=missing-timeout
next_page_results = requests.request(
method,
url,
headers=headers,
params=params,
data=data, # Already serialized above, don't do it again
verify=verify_ssl,
).json()
offset = next_page_results["offset"]
limit = next_page_results["limit"]
# merge results
for k, v in result_json.items():
if isinstance(v, list):
result_json[k] += next_page_results[k]
return result_json
def _get_resource_id(resource):
"""
helper method to find the resource id, since PD API doesn't always return it in the same way
"""
if "id" in resource:
return resource["id"]
if "schedule" in resource:
return resource["schedule"]["id"]
return None
[docs]
def get_resource(
resource_name,
key,
identifier_fields,
profile="pagerduty",
subdomain=None,
api_key=None,
):
"""
Get any single pagerduty resource by key.
CLI Example:
.. code-block:: bash
salt myminion pagerduty.get_resource users "$key" '[name, email]'
We allow flexible lookup by any of a list of identifier_fields.
So, for example, you can look up users by email address or name by calling:
get_resource('users', key, ['name', 'email'], ...)
This method is mainly used to translate state sls into pagerduty id's for dependent objects.
For example, a pagerduty escalation policy contains one or more schedules, which must be passed
by their pagerduty id. We look up the schedules by name (using this method), and then translate
the names into id's.
This method is implemented by getting all objects of the resource type (cached into __context__),
then brute force searching through the list and trying to match any of the identifier_fields.
The __context__ cache is purged after any create, update or delete to the resource.
"""
# cache the expensive 'get all resources' calls into __context__ so that we do them once per salt run
if "pagerduty_util.resource_cache" not in __context__:
__context__["pagerduty_util.resource_cache"] = {}
if resource_name not in __context__["pagerduty_util.resource_cache"]:
if resource_name == "services":
action = resource_name + "?include[]=escalation_policy"
else:
action = resource_name
__context__["pagerduty_util.resource_cache"][resource_name] = _query(
action=action, profile=profile, subdomain=subdomain, api_key=api_key
)[resource_name]
for resource in __context__["pagerduty_util.resource_cache"][resource_name]:
for field in identifier_fields:
if resource[field] == key:
# PagerDuty's /schedules endpoint returns less data than /schedules/:id.
# so, now that we found the schedule, we need to get all the data for it.
if resource_name == "schedules":
full_resource_info = _query(
action=f"{resource_name}/{resource['id']}",
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
return full_resource_info
return resource
return None
[docs]
def create_or_update_resource(
resource_name,
identifier_fields,
data,
diff=None,
profile="pagerduty",
subdomain=None,
api_key=None,
):
"""
create or update any pagerduty resource
Helper method for present().
CLI Example:
Determining if two resources are the same is different for different PD resource, so this method accepts a diff function.
The diff function will be invoked as diff(state_information, object_returned_from_pagerduty), and
should return a dict of data to pass to the PagerDuty update API method, or None if no update
is to be performed. If no diff method is provided, the default behavor is to scan the keys in the state_information,
comparing the matching values in the object_returned_from_pagerduty, and update any values that differ.
examples:
create_or_update_resource("user", ["id","name","email"])
create_or_update_resource("escalation_policies", ["id","name"], diff=my_diff_function)
"""
# try to locate the resource by any of the identifier_fields that are specified in data
resource = None
for field in identifier_fields:
if field in data:
resource = get_resource(
resource_name,
data[field],
identifier_fields,
profile,
subdomain,
api_key,
)
if resource is not None:
break
if resource is None:
if __opts__["test"]:
return "would create"
# flush the resource_cache, because we're modifying a resource
del __context__["pagerduty_util.resource_cache"][resource_name]
# create
return _query(
method="POST",
action=resource_name,
data=data,
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
# update
data_to_update = {}
# if differencing function is provided, use it
if diff:
data_to_update = diff(data, resource)
# else default to naive key-value walk of the dicts
else:
for k, v in data.items():
if k.startswith("_"):
continue
resource_value = resource.get(k, None)
if resource_value is not None and resource_value != v:
data_to_update[k] = v
if data_to_update:
if __opts__["test"]:
return "would update"
# flush the resource_cache, because we're modifying a resource
del __context__["pagerduty_util.resource_cache"][resource_name]
resource_id = _get_resource_id(resource)
return _query(
method="PUT",
action=f"{resource_name}/{resource_id}",
data=data_to_update,
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
return True
[docs]
def delete_resource(
resource_name,
key,
identifier_fields,
profile="pagerduty",
subdomain=None,
api_key=None,
):
"""
delete any pagerduty resource
Helper method for absent()
CLI Example:
.. code-block:: bash
salt myminion pagerduty.delete_resource users "$key" '[id, name, email]'
example:
delete_resource("users", key, ["id","name","email"]) # delete by id or name or email
"""
resource = get_resource(resource_name, key, identifier_fields, profile, subdomain, api_key)
if resource:
if __opts__["test"]:
return "would delete"
# flush the resource_cache, because we're modifying a resource
del __context__["pagerduty_util.resource_cache"][resource_name]
resource_id = _get_resource_id(resource)
return _query(
method="DELETE",
action=f"{resource_name}/{resource_id}",
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
return True
[docs]
def resource_present(
resource,
identifier_fields,
diff=None,
profile="pagerduty",
subdomain=None,
api_key=None,
**kwargs,
):
"""
Generic resource.present state method. Pagerduty state modules should be a thin wrapper over this method,
with a custom diff function.
This method calls create_or_update_resource() and formats the result as a salt state return value.
CLI Example:
example:
resource_present("users", ["id","name","email"])
"""
ret = {"name": kwargs["name"], "changes": {}, "result": None, "comment": ""}
result = create_or_update_resource(
resource,
identifier_fields,
kwargs,
diff=diff,
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
if result is True:
pass
elif result is None:
ret["result"] = True
elif __opts__["test"]:
ret["comment"] = result
elif "error" in result:
ret["result"] = False
ret["comment"] = result
else:
ret["result"] = True
ret["comment"] = result
return ret
[docs]
def resource_absent(
resource, identifier_fields, profile="pagerduty", subdomain=None, api_key=None, **kwargs
):
"""
Generic resource.absent state method. Pagerduty state modules should be a thin wrapper over this method,
with a custom diff function.
This method calls delete_resource() and formats the result as a salt state return value.
CLI Example:
example:
resource_absent("users", ["id","name","email"])
"""
ret = {"name": kwargs["name"], "changes": {}, "result": None, "comment": ""}
for k, v in kwargs.items():
if k not in identifier_fields:
continue
result = delete_resource(
resource,
v,
identifier_fields,
profile=profile,
subdomain=subdomain,
api_key=api_key,
)
if result is None:
ret["result"] = True
ret["comment"] = f"{v} deleted"
return ret
if result is True:
continue
if __opts__["test"]:
ret["comment"] = result
return ret
if "error" in result:
ret["result"] = False
ret["comment"] = result
return ret
return ret