-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #50317 from mitodl/sar/http_beacon
Beacon to check on status endpoint
- Loading branch information
Showing
1 changed file
with
158 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# -*- coding: utf-8 -*- | ||
''' | ||
Beacon to manage and report the status of a server status endpoint. | ||
Fire an event when specified values don't match returned response. | ||
''' | ||
|
||
# Import Python libs | ||
from __future__ import absolute_import, unicode_literals | ||
import logging | ||
import operator | ||
import re | ||
import requests | ||
import itertools | ||
import salt.utils.data | ||
from salt.ext.six.moves import map | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
__virtualname__ = 'http_status' | ||
|
||
comparisons = { | ||
'==': operator.eq, | ||
'<': operator.lt, | ||
'>': operator.gt, | ||
'<=': operator.le, | ||
'>=': operator.ge, | ||
'!=': operator.ne, | ||
'search': re.search | ||
} | ||
|
||
attr_func_map = { | ||
'status': lambda x: x.status_code, | ||
'content': lambda x: x.json() | ||
} | ||
|
||
required_site_attributes = {'url'} | ||
optional_site_attributes = {'content', 'status'} | ||
|
||
|
||
def __virtual__(): | ||
return __virtualname__ | ||
|
||
|
||
def validate(config): | ||
''' | ||
Validate the beacon configuration | ||
''' | ||
valid = True | ||
messages = [] | ||
|
||
if not isinstance(config, list): | ||
valid = False | ||
messages.append('[-] Configuration for %s beacon must be a list', config) | ||
else: | ||
_config = {} | ||
list(map(_config.update, config)) | ||
|
||
try: | ||
sites = _config.get('sites', {}) | ||
except AttributeError: | ||
valid = False | ||
messages.append('[-] Sites for %s beacon must be a dict', __virtualname__) | ||
|
||
if not sites: | ||
valid = False | ||
messages.append('[-] Configuration does not contain sites') | ||
|
||
for site, settings in sites.items(): | ||
if required_site_attributes.isdisjoint(set(settings.keys())): | ||
valid = False | ||
messages.append('[-] Sites for {} beacon requires {}'.format(__virtualname__, | ||
required_site_attributes)) | ||
log.debug('[+] site: %s', site) | ||
log.debug('[+] settings: %s', settings) | ||
|
||
for optional_attrs in itertools.chain(settings.get(attr, []) for attr in optional_site_attributes): | ||
for item in optional_attrs: | ||
cmp = item.get('comp') | ||
if cmp and cmp not in comparisons: | ||
valid = False | ||
messages.append('[-] Invalid comparison operator %s', cmp) | ||
|
||
messages.append('[+] Valid beacon configuration') | ||
return valid, messages | ||
|
||
|
||
def beacon(config): | ||
''' | ||
Check on different service status reported by the django-server-status | ||
library. | ||
.. code-block:: yaml | ||
beacons: | ||
http_status: | ||
- sites: | ||
example-site-1: | ||
url: "https://example.com/status" | ||
timeout: 30 | ||
content-type: json | ||
status: | ||
- value: 400 | ||
comp: < | ||
- value: 300 | ||
comp: '>=' | ||
content: | ||
- path: 'certificate:status' | ||
value: down | ||
comp: '==' | ||
- path: 'status_all' | ||
value: down | ||
comp: '==' | ||
- interval: 10 | ||
''' | ||
ret = [] | ||
|
||
_config = {} | ||
list(map(_config.update, config)) | ||
|
||
for site, site_config in _config.get('sites', {}).items(): | ||
url = site_config.pop('url') | ||
content_type = site_config.pop('content_type', 'json') | ||
try: | ||
r = requests.get(url, timeout=site_config.pop('timeout', 30)) | ||
except requests.exceptions.RequestException as e: | ||
log.info("Request failed: %s", e) | ||
if r.raise_for_status: | ||
log.info('[-] Response from status endpoint was invalid: ' | ||
'%s', r.status_code) | ||
_failed = {'status_code': r.status_code, | ||
'url': url} | ||
ret.append(_failed) | ||
continue | ||
|
||
for attr, checks in site_config.items(): | ||
for check in checks: | ||
log.debug('[+] response_item: %s', attr) | ||
attr_path = check.get('path', '') | ||
comp = comparisons[check['comp']] | ||
expected_value = check['value'] | ||
if attr_path: | ||
received_value = salt.utils.data.traverse_dict_and_list(attr_func_map[attr](r), attr_path) | ||
else: | ||
received_value = attr_func_map[attr](r) | ||
if received_value is None: | ||
log.info('[-] No data found at location {} for url {}'.format(attr_path, url)) | ||
continue | ||
log.debug('[+] expected_value: %s', expected_value) | ||
log.debug('[+] received_value: %s', received_value) | ||
if not comp(expected_value, received_value): | ||
_failed = {'expected': expected_value, | ||
'received': received_value, | ||
'url': url, | ||
'path': attr_path | ||
} | ||
ret.append(_failed) | ||
return ret |