Mini Shell
"""
StatusPage
==========
Manage the StatusPage_ configuration.
.. _StatusPage: https://www.statuspage.io/
In the minion configuration file, the following block is required:
.. code-block:: yaml
statuspage:
api_key: <API_KEY>
page_id: <PAGE_ID>
.. versionadded:: 2017.7.0
"""
import logging
import time
# ----------------------------------------------------------------------------------------------------------------------
# module properties
# ----------------------------------------------------------------------------------------------------------------------
__virtualname__ = "statuspage"
log = logging.getLogger(__file__)
_DO_NOT_COMPARE_FIELDS = ["created_at", "updated_at"]
_MATCH_KEYS = ["id", "name"]
_PACE = 1 # 1 request per second
# ----------------------------------------------------------------------------------------------------------------------
# property functions
# ----------------------------------------------------------------------------------------------------------------------
def __virtual__():
"""
Return the execution module virtualname.
"""
if "statuspage.create" in __salt__:
return True
return (False, "statuspage module could not be loaded")
def _default_ret(name):
"""
Default dictionary returned.
"""
return {"name": name, "result": False, "comment": "", "changes": {}}
def _compute_diff_ret():
"""
Default dictionary retuned by the _compute_diff helper.
"""
return {"add": [], "update": [], "remove": []}
def _clear_dict(endpoint_props):
"""
Eliminates None entries from the features of the endpoint dict.
"""
return {
prop_name: prop_val
for prop_name, prop_val in endpoint_props.items()
if prop_val is not None
}
def _ignore_keys(endpoint_props):
"""
Ignores some keys that might be different without any important info.
These keys are defined under _DO_NOT_COMPARE_FIELDS.
"""
return {
prop_name: prop_val
for prop_name, prop_val in endpoint_props.items()
if prop_name not in _DO_NOT_COMPARE_FIELDS
}
def _unique(list_of_dicts):
"""
Returns an unique list of dictionaries given a list that may contain duplicates.
"""
unique_list = []
for ele in list_of_dicts:
if ele not in unique_list:
unique_list.append(ele)
return unique_list
def _clear_ignore(endpoint_props):
"""
Both _clear_dict and _ignore_keys in a single iteration.
"""
return {
prop_name: prop_val
for prop_name, prop_val in endpoint_props.items()
if prop_name not in _DO_NOT_COMPARE_FIELDS and prop_val is not None
}
def _clear_ignore_list(lst):
"""
Apply _clear_ignore to a list.
"""
return _unique([_clear_ignore(ele) for ele in lst])
def _find_match(ele, lst):
"""
Find a matching element in a list.
"""
for _ele in lst:
for match_key in _MATCH_KEYS:
if _ele.get(match_key) == ele.get(match_key):
return ele
def _update_on_fields(prev_ele, new_ele):
"""
Return a dict with fields that differ between two dicts.
"""
fields_update = {
prop_name: prop_val
for prop_name, prop_val in new_ele.items()
if new_ele.get(prop_name) != prev_ele.get(prop_name) or prop_name in _MATCH_KEYS
}
if len(set(fields_update.keys()) | set(_MATCH_KEYS)) > len(set(_MATCH_KEYS)):
if "id" not in fields_update:
# in case of update, the ID is necessary
# if not specified in the pillar,
# will try to get it from the prev_ele
fields_update["id"] = prev_ele["id"]
return fields_update
def _compute_diff(expected_endpoints, configured_endpoints):
"""
Compares configured endpoints with the expected configuration and returns the differences.
"""
new_endpoints = []
update_endpoints = []
remove_endpoints = []
ret = _compute_diff_ret()
# noth configured => configure with expected endpoints
if not configured_endpoints:
ret.update({"add": expected_endpoints})
return ret
# noting expected => remove everything
if not expected_endpoints:
ret.update({"remove": configured_endpoints})
return ret
expected_endpoints_clear = _clear_ignore_list(expected_endpoints)
configured_endpoints_clear = _clear_ignore_list(configured_endpoints)
for expected_endpoint_clear in expected_endpoints_clear:
if expected_endpoint_clear not in configured_endpoints_clear:
# none equal => add or update
matching_ele = _find_match(
expected_endpoint_clear, configured_endpoints_clear
)
if not matching_ele:
# new element => add
new_endpoints.append(expected_endpoint_clear)
else:
# element matched, but some fields are different
update_fields = _update_on_fields(matching_ele, expected_endpoint_clear)
if update_fields:
update_endpoints.append(update_fields)
for configured_endpoint_clear in configured_endpoints_clear:
if configured_endpoint_clear not in expected_endpoints_clear:
matching_ele = _find_match(
configured_endpoint_clear, expected_endpoints_clear
)
if not matching_ele:
# no match found => remove
remove_endpoints.append(configured_endpoint_clear)
return {
"add": new_endpoints,
"update": update_endpoints,
"remove": remove_endpoints,
}
# ----------------------------------------------------------------------------------------------------------------------
# callable functions
# ----------------------------------------------------------------------------------------------------------------------
def create(
name,
endpoint="incidents",
api_url=None,
page_id=None,
api_key=None,
api_version=None,
**kwargs,
):
"""
Insert a new entry under a specific endpoint.
endpoint: incidents
Insert under this specific endpoint.
page_id
Page ID. Can also be specified in the config file.
api_key
API key. Can also be specified in the config file.
api_version: 1
API version. Can also be specified in the config file.
api_url
Custom API URL in case the user has a StatusPage service running in a custom environment.
kwargs
Other params.
SLS Example:
.. code-block:: yaml
create-my-component:
statuspage.create:
- endpoint: components
- name: my component
- group_id: 993vgplshj12
"""
ret = _default_ret(name)
endpoint_sg = endpoint[:-1] # singular
if __opts__["test"]:
ret["comment"] = "The following {endpoint} would be created:".format(
endpoint=endpoint_sg
)
ret["result"] = None
ret["changes"][endpoint] = {}
for karg, warg in kwargs.items():
if warg is None or karg.startswith("__"):
continue
ret["changes"][endpoint][karg] = warg
return ret
sp_create = __salt__["statuspage.create"](
endpoint=endpoint,
api_url=api_url,
page_id=page_id,
api_key=api_key,
api_version=api_version,
**kwargs,
)
if not sp_create.get("result"):
ret["comment"] = "Unable to create {endpoint}: {msg}".format(
endpoint=endpoint_sg, msg=sp_create.get("comment")
)
else:
ret["comment"] = f"{endpoint_sg} created!"
ret["result"] = True
ret["changes"] = sp_create.get("out")
def update(
name,
endpoint="incidents",
id=None,
api_url=None,
page_id=None,
api_key=None,
api_version=None,
**kwargs,
):
"""
Update attribute(s) of a specific endpoint.
id
The unique ID of the endpoint entry.
endpoint: incidents
Endpoint name.
page_id
Page ID. Can also be specified in the config file.
api_key
API key. Can also be specified in the config file.
api_version: 1
API version. Can also be specified in the config file.
api_url
Custom API URL in case the user has a StatusPage service running in a custom environment.
SLS Example:
.. code-block:: yaml
update-my-incident:
statuspage.update:
- id: dz959yz2nd4l
- status: resolved
"""
ret = _default_ret(name)
endpoint_sg = endpoint[:-1] # singular
if not id:
log.error("Invalid %s ID", endpoint_sg)
ret["comment"] = "Please specify a valid {endpoint} ID".format(
endpoint=endpoint_sg
)
return ret
if __opts__["test"]:
ret["comment"] = "{endpoint} #{id} would be updated:".format(
endpoint=endpoint_sg, id=id
)
ret["result"] = None
ret["changes"][endpoint] = {}
for karg, warg in kwargs.items():
if warg is None or karg.startswith("__"):
continue
ret["changes"][endpoint][karg] = warg
return ret
sp_update = __salt__["statuspage.update"](
endpoint=endpoint,
id=id,
api_url=api_url,
page_id=page_id,
api_key=api_key,
api_version=api_version,
**kwargs,
)
if not sp_update.get("result"):
ret["comment"] = "Unable to update {endpoint} #{id}: {msg}".format(
endpoint=endpoint_sg, id=id, msg=sp_update.get("comment")
)
else:
ret["comment"] = f"{endpoint_sg} #{id} updated!"
ret["result"] = True
ret["changes"] = sp_update.get("out")
def delete(
name,
endpoint="incidents",
id=None,
api_url=None,
page_id=None,
api_key=None,
api_version=None,
):
"""
Remove an entry from an endpoint.
endpoint: incidents
Request a specific endpoint.
page_id
Page ID. Can also be specified in the config file.
api_key
API key. Can also be specified in the config file.
api_version: 1
API version. Can also be specified in the config file.
api_url
Custom API URL in case the user has a StatusPage service running in a custom environment.
SLS Example:
.. code-block:: yaml
delete-my-component:
statuspage.delete:
- endpoint: components
- id: ftgks51sfs2d
"""
ret = _default_ret(name)
endpoint_sg = endpoint[:-1] # singular
if not id:
log.error("Invalid %s ID", endpoint_sg)
ret["comment"] = "Please specify a valid {endpoint} ID".format(
endpoint=endpoint_sg
)
return ret
if __opts__["test"]:
ret["comment"] = "{endpoint} #{id} would be removed!".format(
endpoint=endpoint_sg, id=id
)
ret["result"] = None
sp_delete = __salt__["statuspage.delete"](
endpoint=endpoint,
id=id,
api_url=api_url,
page_id=page_id,
api_key=api_key,
api_version=api_version,
)
if not sp_delete.get("result"):
ret["comment"] = "Unable to delete {endpoint} #{id}: {msg}".format(
endpoint=endpoint_sg, id=id, msg=sp_delete.get("comment")
)
else:
ret["comment"] = f"{endpoint_sg} #{id} deleted!"
ret["result"] = True
def managed(
name,
config,
api_url=None,
page_id=None,
api_key=None,
api_version=None,
pace=_PACE,
allow_empty=False,
):
"""
Manage the StatusPage configuration.
config
Dictionary with the expected configuration of the StatusPage.
The main level keys of this dictionary represent the endpoint name.
If a certain endpoint does not exist in this structure, it will be ignored / not configured.
page_id
Page ID. Can also be specified in the config file.
api_key
API key. Can also be specified in the config file.
api_version: 1
API version. Can also be specified in the config file.
api_url
Custom API URL in case the user has a StatusPage service running in a custom environment.
pace: 1
Max requests per second allowed by the API.
allow_empty: False
Allow empty config.
SLS example:
.. code-block:: yaml
my-statuspage-config:
statuspage.managed:
- config:
components:
- name: component1
group_id: uy4g37rf
- name: component2
group_id: 3n4uyu4gf
incidents:
- name: incident1
status: resolved
impact: major
backfilled: false
- name: incident2
status: investigating
impact: minor
"""
complete_diff = {}
ret = _default_ret(name)
if not config and not allow_empty:
ret.update(
{
"result": False,
"comment": (
"Cannot remove everything. To allow this, please set the option"
" `allow_empty` as True."
),
}
)
return ret
is_empty = True
for endpoint_name, endpoint_expected_config in config.items():
if endpoint_expected_config:
is_empty = False
endpoint_existing_config_ret = __salt__["statuspage.retrieve"](
endpoint=endpoint_name,
api_url=api_url,
page_id=page_id,
api_key=api_key,
api_version=api_version,
)
if not endpoint_existing_config_ret.get("result"):
ret.update({"comment": endpoint_existing_config_ret.get("comment")})
return ret # stop at first error
endpoint_existing_config = endpoint_existing_config_ret.get("out")
complete_diff[endpoint_name] = _compute_diff(
endpoint_expected_config, endpoint_existing_config
)
if is_empty and not allow_empty:
ret.update(
{
"result": False,
"comment": (
"Cannot remove everything. To allow this, please set the option"
" `allow_empty` as True."
),
}
)
return ret
any_changes = False
for endpoint_name, endpoint_diff in complete_diff.items():
if (
endpoint_diff.get("add")
or endpoint_diff.get("update")
or endpoint_diff.get("remove")
):
any_changes = True
if not any_changes:
ret.update({"result": True, "comment": "No changes required.", "changes": {}})
return ret
ret.update({"changes": complete_diff})
if __opts__.get("test"):
ret.update(
{
"comment": "Testing mode. Would apply the following changes:",
"result": None,
}
)
return ret
for endpoint_name, endpoint_diff in complete_diff.items():
endpoint_sg = endpoint_name[:-1] # singular
for new_endpoint in endpoint_diff.get("add"):
log.debug("Defining new %s %s", endpoint_sg, new_endpoint)
adding = __salt__["statuspage.create"](
endpoint=endpoint_name,
api_url=api_url,
page_id=page_id,
api_key=api_key,
api_version=api_version,
**new_endpoint,
)
if not adding.get("result"):
ret.update({"comment": adding.get("comment")})
return ret
if pace:
time.sleep(1 / pace)
for update_endpoint in endpoint_diff.get("update"):
if "id" not in update_endpoint:
continue
endpoint_id = update_endpoint.pop("id")
log.debug("Updating %s #%s: %s", endpoint_sg, endpoint_id, update_endpoint)
updating = __salt__["statuspage.update"](
endpoint=endpoint_name,
id=endpoint_id,
api_url=api_url,
page_id=page_id,
api_key=api_key,
api_version=api_version,
**update_endpoint,
)
if not updating.get("result"):
ret.update({"comment": updating.get("comment")})
return ret
if pace:
time.sleep(1 / pace)
for remove_endpoint in endpoint_diff.get("remove"):
if "id" not in remove_endpoint:
continue
endpoint_id = remove_endpoint.pop("id")
log.debug("Removing %s #%s", endpoint_sg, endpoint_id)
removing = __salt__["statuspage.delete"](
endpoint=endpoint_name,
id=endpoint_id,
api_url=api_url,
page_id=page_id,
api_key=api_key,
api_version=api_version,
)
if not removing.get("result"):
ret.update({"comment": removing.get("comment")})
return ret
if pace:
time.sleep(1 / pace)
ret.update({"result": True, "comment": "StatusPage updated."})
return ret
Zerion Mini Shell 1.0