File: //proc/self/root/usr/lib/python2.7/site-packages/salt/modules/telemetry.py
# -*- coding: utf-8 -*-
'''
Connection module for Telemetry
.. versionadded:: 2016.3.0
https://github.com/mongolab/mongolab-telemetry-api-docs/blob/master/alerts.md
:configuration: This module accepts explicit telemetry credentials or
can also read api key credentials from a pillar. More Information available
here__.
.. __: https://github.com/mongolab/mongolab-telemetry-api-docs/blob/master/alerts.md
In the minion's config file:
.. code-block:: yaml
telemetry.telemetry_api_keys:
- abc123 # Key 1
- efg321 # Backup Key 1
telemetry_api_base_url: https://telemetry-api.mongolab.com/v0
:depends: requests
'''
# Import Python libs
from __future__ import absolute_import, unicode_literals, print_function
import logging
# Import Salt libs
import salt.utils.json
import salt.utils.stringutils
# Import 3rd-party libs
from salt.ext import six
log = logging.getLogger(__name__)
# Import third party libs
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
__virtualname__ = 'telemetry'
def __virtual__():
# Only load if imports exist.
if not HAS_REQUESTS:
return False
return __virtualname__
def _get_telemetry_base(profile):
config = __salt__['config.option'](profile)
return config.get('telemetry_api_base_url')
def _auth(api_key=None, profile='telemetry'):
# return telemetry api key in the header
if api_key is None and profile is None:
raise Exception("Missing api_key and profile")
if profile:
if isinstance(profile, six.string_types):
_profile = __salt__['config.option'](profile)
elif isinstance(profile, dict):
_profile = profile
if _profile:
api_key = _profile.get('telemetry_api_keys')[0]
else:
raise Exception("Missing api_key")
return {'Telemetry-API-Key': api_key, 'content-type': 'application/json'}
def _update_cache(deployment_id, metric_name, alert):
key = "telemetry.{0}.alerts".format(deployment_id)
if key in __context__:
alerts = __context__[key]
alerts[metric_name] = alert
__context__[key] = alerts
return __context__.get(key, [])
def _retrieve_channel_id(email, profile='telemetry'):
'''
Given an email address, checks the local
cache if corresponding email address: channel_id
mapping exists
email
Email escalation policy
profile
A dict of telemetry config information.
'''
key = "telemetry.channels"
auth = _auth(profile=profile)
if key not in __context__:
get_url = _get_telemetry_base(profile) + "/notification-channels?_type=EmailNotificationChannel"
response = requests.get(get_url, headers=auth)
if response.status_code == 200:
cache_result = {}
for index, alert in enumerate(response.json()):
cache_result[alert.get('email')] = alert.get('_id', 'false')
__context__[key] = cache_result
return __context__[key].get(email, False)
def get_alert_config(deployment_id, metric_name=None, api_key=None, profile="telemetry"):
'''
Get all alert definitions associated with a given deployment or if metric_name
is specified, obtain the specific alert config
Returns dictionary or list of dictionaries.
CLI Example:
salt myminion telemetry.get_alert_config rs-ds033197 currentConnections profile=telemetry
salt myminion telemetry.get_alert_config rs-ds033197 profile=telemetry
'''
auth = _auth(profile=profile)
alert = False
key = "telemetry.{0}.alerts".format(deployment_id)
if key not in __context__:
try:
get_url = _get_telemetry_base(profile) + "/alerts?deployment={0}".format(deployment_id)
response = requests.get(get_url, headers=auth)
except requests.exceptions.RequestException as e:
log.error(six.text_type(e))
return False
http_result = {}
if response.status_code == 200:
for alert in response.json():
http_result[alert.get('condition', {}).get('metric')] = alert
__context__[key] = http_result
if not __context__.get(key):
return []
alerts = __context__[key].values()
if metric_name:
return __context__[key].get(metric_name)
return [alert['_id'] for alert in alerts if '_id' in alert]
def get_notification_channel_id(notify_channel, profile="telemetry"):
'''
Given an email address, creates a notification-channels
if one is not found and also returns the corresponding
notification channel id.
notify_channel
Email escalation policy
profile
A dict of telemetry config information.
CLI Example:
salt myminion telemetry.get_notification_channel_id userx@company.com profile=telemetry
'''
# This helper is used to procure the channel ids
# used to notify when the alarm threshold is violated
auth = _auth(profile=profile)
notification_channel_id = _retrieve_channel_id(notify_channel)
if not notification_channel_id:
log.info("%s channel does not exist, creating.", notify_channel)
# create the notification channel and cache the id
post_url = _get_telemetry_base(profile) + "/notification-channels"
data = {
"_type": "EmailNotificationChannel",
"name": notify_channel[:notify_channel.find('@')] + 'EscalationPolicy',
"email": notify_channel
}
response = requests.post(post_url, data=salt.utils.json.dumps(data), headers=auth)
if response.status_code == 200:
log.info("Successfully created EscalationPolicy %s with EmailNotificationChannel %s",
data.get('name'), notify_channel)
notification_channel_id = response.json().get('_id')
__context__["telemetry.channels"][notify_channel] = notification_channel_id
else:
raise Exception("Failed to created notification channel {0}".format(notify_channel))
return notification_channel_id
def get_alarms(deployment_id, profile="telemetry"):
'''
get all the alarms set up against the current deployment
Returns dictionary of alarm information
CLI Example:
salt myminion telemetry.get_alarms rs-ds033197 profile=telemetry
'''
auth = _auth(profile=profile)
try:
response = requests.get(_get_telemetry_base(profile) + "/alerts?deployment={0}".format(deployment_id), headers=auth)
except requests.exceptions.RequestException as e:
log.error(six.text_type(e))
return False
if response.status_code == 200:
alarms = response.json()
if len(alarms) > 0:
return alarms
return 'No alarms defined for deployment: {0}'.format(deployment_id)
else:
# Non 200 response, sent back the error response'
return {'err_code': response.status_code, 'err_msg': salt.utils.json.loads(response.text).get('err', '')}
def create_alarm(deployment_id, metric_name, data, api_key=None, profile="telemetry"):
'''
create an telemetry alarms.
data is a dict of alert configuration data.
Returns (bool success, str message) tuple.
CLI Example:
salt myminion telemetry.create_alarm rs-ds033197 {} profile=telemetry
'''
auth = _auth(api_key, profile)
request_uri = _get_telemetry_base(profile) + "/alerts"
key = "telemetry.{0}.alerts".format(deployment_id)
# set the notification channels if not already set
post_body = {
"deployment": deployment_id,
"filter": data.get('filter'),
"notificationChannel": get_notification_channel_id(data.get('escalate_to')).split(),
"condition": {
"metric": metric_name,
"max": data.get('max'),
"min": data.get('min')
}
}
try:
response = requests.post(request_uri, data=salt.utils.json.dumps(post_body), headers=auth)
except requests.exceptions.RequestException as e:
# TODO: May be we should retry?
log.error(six.text_type(e))
if response.status_code >= 200 and response.status_code < 300:
# update cache
log.info('Created alarm on metric: %s in deployment: %s', metric_name, deployment_id)
log.debug('Updating cache for metric %s in deployment %s: %s',
metric_name, deployment_id, response.json())
_update_cache(deployment_id, metric_name, response.json())
else:
log.error(
'Failed to create alarm on metric: %s in '
'deployment %s: payload: %s',
metric_name, deployment_id, salt.utils.json.dumps(post_body)
)
return response.status_code >= 200 and response.status_code < 300, response.json()
def update_alarm(deployment_id, metric_name, data, api_key=None, profile="telemetry"):
'''
update an telemetry alarms. data is a dict of alert configuration data.
Returns (bool success, str message) tuple.
CLI Example:
salt myminion telemetry.update_alarm rs-ds033197 {} profile=telemetry
'''
auth = _auth(api_key, profile)
alert = get_alert_config(deployment_id, metric_name, api_key, profile)
if not alert:
return False, "No entity found matching deployment {0} and alarms {1}".format(deployment_id, metric_name)
request_uri = _get_telemetry_base(profile) + '/alerts/' + alert['_id']
# set the notification channels if not already set
post_body = {
"deployment": deployment_id,
"filter": data.get('filter'),
"notificationChannel": get_notification_channel_id(data.get('escalate_to')).split(),
"condition": {
"metric": metric_name,
"max": data.get('max'),
"min": data.get('min')
}
}
try:
response = requests.put(request_uri, data=salt.utils.json.dumps(post_body), headers=auth)
except requests.exceptions.RequestException as e:
log.error('Update failed: %s', e)
return False, six.text_type(e)
if response.status_code >= 200 and response.status_code < 300:
# Also update cache
log.debug('Updating cache for metric %s in deployment %s: %s',
metric_name, deployment_id, response.json())
_update_cache(deployment_id, metric_name, response.json())
log.info('Updated alarm on metric: %s in deployment: %s', metric_name, deployment_id)
return True, response.json()
err_msg = six.text_type( # future lint: disable=blacklisted-function
'Failed to create alarm on metric: {0} in deployment: {1} '
'payload: {2}').format(
salt.utils.stringutils.to_unicode(metric_name),
salt.utils.stringutils.to_unicode(deployment_id),
salt.utils.json.dumps(post_body)
)
log.error(err_msg)
return False, err_msg
def delete_alarms(deployment_id, alert_id=None, metric_name=None, api_key=None, profile='telemetry'):
'''delete an alert specified by alert_id or if not specified blows away all the alerts
in the current deployment.
Returns (bool success, str message) tuple.
CLI Example:
salt myminion telemetry.delete_alarms rs-ds033197 profile=telemetry
'''
auth = _auth(profile=profile)
if alert_id is None:
# Delete all the alarms associated with this deployment
alert_ids = get_alert_config(deployment_id, api_key=api_key, profile=profile)
else:
alert_ids = [alert_id]
if len(alert_ids) == 0:
return False, "failed to find alert associated with deployment: {0}".format(deployment_id)
failed_to_delete = []
for id in alert_ids:
delete_url = _get_telemetry_base(profile) + "/alerts/{0}".format(id)
try:
response = requests.delete(delete_url, headers=auth)
if metric_name:
log.debug("updating cache and delete %s key from %s",
metric_name, deployment_id)
_update_cache(deployment_id, metric_name, None)
except requests.exceptions.RequestException as e:
log.error('Delete failed: %s', e)
if response.status_code != 200:
failed_to_delete.append(id)
if len(failed_to_delete) > 0:
return False, "Failed to delete {0} alarms in deployment: {1}" .format(', '.join(failed_to_delete), deployment_id)
return True, "Successfully deleted {0} alerts in deployment: {1}".format(', '.join(alert_ids), deployment_id)