Skip to content

Commit 4cfbe52

Browse files
authored
Merge pull request #23 from absolutejam/feat/handler_support
add handler support!
2 parents 20bf988 + 005cee2 commit 4cfbe52

File tree

3 files changed

+302
-4
lines changed

3 files changed

+302
-4
lines changed

requirements_test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ nose
33
pep8
44
pylint
55
coverage
6+
requests

sensu_plugin/handler.py

Lines changed: 233 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,245 @@
66
# Released under the same terms as Sensu (the MIT license); see LICENSE
77
# for details.
88

9+
'''
10+
This provides a base SensuHandler class that can be used for writing
11+
python-based Sensu handlers.
12+
'''
13+
14+
from __future__ import print_function
15+
import os
16+
import sys
17+
import json
18+
import requests
19+
try:
20+
from urlparse import urlparse
21+
except ImportError:
22+
from urllib.parse import urlparse
23+
from sensu_plugin.utils import get_settings
24+
925

1026
class SensuHandler(object):
27+
'''
28+
Class to be used as a basis for handlers.
29+
'''
1130
def __init__(self):
12-
pass
31+
# Parse the stdin into a global event object
32+
stdin = sys.stdin.read()
33+
self.read_event(stdin)
34+
35+
# Prepare global settings
36+
self.settings = get_settings()
37+
self.api_settings = self.get_api_settings()
38+
39+
# Filter (deprecated) and handle
40+
self.filter()
41+
self.handle()
42+
43+
def read_event(self, check_result):
44+
'''
45+
Convert the piped check result (json) into a global 'event' dict
46+
'''
47+
try:
48+
self.event = json.loads(check_result)
49+
self.event['occurrences'] = self.event.get('occurrences', 1)
50+
self.event['check'] = self.event.get('check', {})
51+
self.event['client'] = self.event.get('client', {})
52+
except Exception as exception: # pylint: disable=broad-except
53+
print('error reading event: ' + exception)
54+
sys.exit(1)
1355

1456
def handle(self):
15-
pass
57+
'''
58+
Method that should be overwritten to provide handler logic.
59+
'''
60+
print('ignoring event -- no handler defined')
1661

1762
def filter(self):
18-
pass
63+
'''
64+
Filters exit the proccess if the event should not be handled.
65+
Filtering events is deprecated and will be removed in a future release.
66+
'''
67+
68+
if self.deprecated_filtering_enabled():
69+
print('warning: event filtering in sensu-plugin is deprecated,' +
70+
'see http://bit.ly/sensu-plugin')
71+
self.filter_disabled()
72+
self.filter_silenced()
73+
self.filter_dependencies()
74+
75+
if self.deprecated_occurrence_filtering():
76+
print('warning: occurrence filtering in sensu-plugin is' +
77+
'deprecated, see http://bit.ly/sensu-plugin')
78+
self.filter_repeated()
79+
80+
def deprecated_filtering_enabled(self):
81+
'''
82+
Evaluates whether the event should be processed by any of the
83+
filter methods in this library. Defaults to true,
84+
i.e. deprecated filters are run by default.
85+
86+
returns bool
87+
'''
88+
return self.event['check'].get('enable_deprecated_filtering', False)
89+
90+
def deprecated_occurrence_filtering(self):
91+
'''
92+
Evaluates whether the event should be processed by the
93+
filter_repeated method. Defaults to true, i.e. filter_repeated
94+
will filter events by default.
95+
96+
returns bool
97+
'''
98+
99+
return self.event['check'].get(
100+
'enable_deprecated_occurrence_filtering', False)
19101

20102
def bail(self, msg):
21-
pass
103+
'''
104+
Gracefully terminate with message
105+
'''
106+
client_name = self.event.get('client', 'error:no-client-name')
107+
check_name = self.event['client'].get('name', 'error:no-check-name')
108+
print('{}: {}/{}'.format(msg, client_name, check_name))
109+
sys.exit(0)
110+
111+
def get_api_settings(self):
112+
'''
113+
Return a hash of API settings derived first from ENV['sensu_api_url']
114+
if set, then Sensu config `api` scope if configured, and finally
115+
falling back to to ipv4 localhost address on default API port.
116+
117+
return dict
118+
'''
119+
120+
sensu_api_url = os.environ.get('sensu_api_url')
121+
if sensu_api_url:
122+
uri = urlparse(sensu_api_url)
123+
self.api_settings = {
124+
'host': '{0}//{1}'.format(uri.scheme, uri.hostname),
125+
'port': uri.port,
126+
'user': uri.username,
127+
'password': uri.password
128+
}
129+
else:
130+
self.api_settings = self.settings.get('api', {})
131+
self.api_settings['host'] = self.api_settings.get(
132+
'host', '127.0.0.1')
133+
self.api_settings['port'] = self.api_settings.get(
134+
'port', 4567)
135+
136+
# API requests
137+
def api_request(self, method, path):
138+
'''
139+
Query Sensu api for information.
140+
'''
141+
if not hasattr(self, 'api_settings'):
142+
ValueError('api.json settings not found')
143+
144+
if method.lower() == 'get':
145+
_request = requests.get
146+
elif method.lower() == 'post':
147+
_request = requests.post
148+
149+
domain = self.api_settings['host']
150+
uri = 'http://{}:{}{}'.format(domain, self.api_settings['port'], path)
151+
if self.api_settings['user'] and self.api_settings['password']:
152+
auth = (self.api_settings['user'], self.api_settings['password'])
153+
else:
154+
auth = ()
155+
req = _request(uri, auth=auth)
156+
return req
157+
158+
def stash_exists(self, path):
159+
'''
160+
Query Sensu API for stash data.
161+
'''
162+
return self.api_request('get', '/stash' + path).status_code == 200
163+
164+
def event_exists(self, client, check):
165+
'''
166+
Query Sensu API for event.
167+
'''
168+
return self.api_request(
169+
'get',
170+
'events/{}/{}'.format(client, check)
171+
).status_code == 200
172+
173+
# Filters
174+
def filter_disabled(self):
175+
'''
176+
Determine whether a check is disabled and shouldn't handle.
177+
'''
178+
if self.event['check']['alert'] is False:
179+
self.bail('alert disabled')
180+
181+
def filter_silenced(self):
182+
'''
183+
Determine whether a check is silenced and shouldn't handle.
184+
'''
185+
stashes = [
186+
('client', '/silence/{}'.format(self.event['client']['name'])),
187+
('check', '/silence/{}/{}'.format(
188+
self.event['client']['name'],
189+
self.event['check']['name'])),
190+
('check', '/silence/all/{}'.format(self.event['check']['name']))
191+
]
192+
for scope, path in stashes:
193+
if self.stash_exists(path):
194+
self.bail(scope + ' alerts silenced')
195+
196+
def filter_dependencies(self):
197+
'''
198+
Determine whether a check has dependencies.
199+
'''
200+
dependencies = self.event['check'].get('dependencies', None)
201+
if dependencies is None or not isinstance(dependencies, list):
202+
return
203+
for dependency in self.event['check']['dependencies']:
204+
if not str(dependency):
205+
continue
206+
dependency_split = tuple(dependency.split('/'))
207+
# If there's a dependency on a check from another client, then use
208+
# that client name, otherwise assume same client.
209+
if len(dependency_split) == 2:
210+
client, check = dependency_split
211+
else:
212+
client = self.event['client']['name']
213+
check = dependency_split[0]
214+
if self.event_exists(client, check):
215+
self.bail('check dependency event exists')
216+
217+
def filter_repeated(self):
218+
'''
219+
Determine whether a check is repeating.
220+
'''
221+
defaults = {
222+
'occurrences': 1,
223+
'interval': 30,
224+
'refresh': 1800
225+
}
226+
227+
# Override defaults with anything defined in the settings
228+
if isinstance(self.settings['sensu_plugin'], dict):
229+
defaults.update(self.settings['sensu_plugin'])
230+
231+
occurrences = int(self.event['check'].get(
232+
'occurrences', defaults['occurrences']))
233+
interval = int(self.event['check'].get(
234+
'interval', defaults['interval']))
235+
refresh = int(self.event['check'].get(
236+
'refresh', defaults['refresh']))
237+
238+
if self.event['occurrences'] < occurrences:
239+
self.bail('not enough occurrences')
240+
241+
if (self.event['occurrences'] > occurrences and
242+
self.event['action'] == 'create'):
243+
return
244+
245+
number = int(refresh / interval)
246+
if (number == 0 or
247+
(self.event['occurrences'] - occurrences) % number == 0):
248+
return
249+
250+
self.bail('only handling every ' + str(number) + ' occurrences')

sensu_plugin/utils/__init__.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'''
2+
Utilities for loading config files, etc.
3+
'''
4+
5+
import os
6+
import json
7+
8+
9+
def config_files():
10+
'''
11+
Get list of currently used config files.
12+
'''
13+
sensu_loaded_tempfile = os.environ.get('SENSU_LOADED_TEMPFILE')
14+
sensu_config_files = os.environ.get('SENSU_CONFIG_FILES')
15+
if sensu_loaded_tempfile and os.path.isfile(sensu_loaded_tempfile):
16+
with open(sensu_loaded_tempfile, 'r') as tempfile:
17+
contents = tempfile.read()
18+
return contents.split(':')
19+
elif sensu_config_files:
20+
return sensu_config_files.split(':')
21+
else:
22+
files = ['/etc/sensu/config.json']
23+
return [files.append('/etc/sensu/conf.d/{}'.format(filename))
24+
for filename in os.listdir('/etc/sensu/conf.d')
25+
if os.path.splitext(filename)[1] == '.json']
26+
27+
28+
def get_settings():
29+
'''
30+
Get all currently loaded settings.
31+
'''
32+
settings = {}
33+
for config_file in config_files():
34+
config_contents = load_config(config_file)
35+
if config_contents is not None:
36+
settings = deep_merge(settings, config_contents)
37+
return settings
38+
39+
40+
def load_config(filename):
41+
'''
42+
Read contents of config file.
43+
'''
44+
try:
45+
with open(filename, 'r') as config_file:
46+
return json.loads(config_file.read())
47+
except IOError:
48+
pass
49+
50+
51+
def deep_merge(dict_one, dict_two):
52+
'''
53+
Deep merge two dicts.
54+
'''
55+
merged = dict_one.copy()
56+
for key, value in dict_two.items():
57+
# value is equivalent to dict_two[key]
58+
if (key in dict_one and
59+
isinstance(dict_one[key], dict) and
60+
isinstance(value, dict)):
61+
merged[key] = deep_merge(dict_one[key], value)
62+
elif (key in dict_one and
63+
isinstance(dict_one[key], list) and
64+
isinstance(value, list)):
65+
merged[key] = list(set(dict_one[key] + value))
66+
else:
67+
merged[key] = value
68+
return merged

0 commit comments

Comments
 (0)