File: //usr/lib/python2.7/site-packages/salt/states/statuspage.py
# -*- coding: utf-8 -*-
'''
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
'''
from __future__ import absolute_import, unicode_literals, print_function
# import python std lib
import time
import logging
# import salt
from salt.ext import six
# ----------------------------------------------------------------------------------------------------------------------
# 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.
'''
return __virtualname__
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 dict(
(prop_name, prop_val)
for prop_name, prop_val in six.iteritems(endpoint_props)
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 dict(
(prop_name, prop_val)
for prop_name, prop_val in six.iteritems(endpoint_props)
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 dict(
(prop_name, prop_val)
for prop_name, prop_val in six.iteritems(endpoint_props)
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 = dict(
(prop_name, prop_val)
for prop_name, prop_val in six.iteritems(new_ele)
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 six.iteritems(kwargs):
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'] = '{endpoint} created!'.format(endpoint=endpoint_sg)
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 enpoint 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 six.iteritems(kwargs):
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'] = '{endpoint} #{id} updated!'.format(endpoint=endpoint_sg, id=id)
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'] = '{endpoint} #{id} deleted!'.format(endpoint=endpoint_sg, id=id)
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 six.iteritems(config):
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 six.iteritems(complete_diff):
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 six.iteritems(complete_diff):
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