Completed
Pull Request — master (#396)
by
unknown
01:51
created

_check_stash()   B

Complexity

Conditions 5

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 5
dl 0
loc 28
rs 8.0894
1
#!/usr/bin/env python
2
3
import argparse
4
import base64
5
import httplib
6
try:
7
    import simplejson as json
8
except ImportError:
9
    import json
10
import os
11
import sys
12
import traceback
13
from urlparse import urljoin
14
15
try:
16
    import requests
17
    requests.packages.urllib3.disable_warnings()
18
except ImportError:
19
    raise ImportError('Missing dependency "requests". \
20
        Do ``pip install requests``.')
21
22
try:
23
    import yaml
24
except ImportError:
25
    raise ImportError('Missing dependency "pyyaml". \
26
        Do ``pip install pyyaml``.')
27
28
# ST2 configuration
29
30
ST2_API_BASE_URL = 'https://localhost/api/v1/'
31
ST2_AUTH_BASE_URL = 'https://localhost/auth/v1/'
32
ST2_USERNAME = None
33
ST2_PASSWORD = None
34
ST2_API_KEY = None
35
ST2_AUTH_TOKEN = None
36
ST2_SSL_VERFIFY = False
37
38
ST2_AUTH_PATH = 'tokens'
39
ST2_WEBHOOKS_PATH = 'webhooks/st2'
40
ST2_TRIGGERS_PATH = 'triggertypes/'
41
ST2_TRIGGERTYPE_PACK = 'sensu'
42
ST2_TRIGGERTYPE_NAME = 'event_handler'
43
ST2_TRIGGERTYPE_REF = '.'.join([ST2_TRIGGERTYPE_PACK, ST2_TRIGGERTYPE_NAME])
44
45
# Sensu configuration
46
47
SENSU_HOST = 'localhost'
48
SENSU_PORT = 4567
49
SENSU_USER = ''
50
SENSU_PASS = ''
51
52
REGISTERED_WITH_ST2 = False
53
UNAUTHED = False
54
IS_API_KEY_AUTH = False
55
56
OK_CODES = [httplib.OK, httplib.CREATED, httplib.ACCEPTED, httplib.CONFLICT]
57
UNREACHABLE_CODES = [httplib.NOT_FOUND]
58
59
TOKEN_AUTH_HEADER = 'X-Auth-Token'
60
API_KEY_AUTH_HEADER = 'St2-Api-Key'
61
62
63
def _get_sensu_request_headers():
64
    b64auth = base64.b64encode(
65
        "%s:%s" %
66
        (SENSU_USER, SENSU_PASS))
67
    auth_header = "BASIC %s" % b64auth
68
    content_header = "application/json"
69
    return {"Authorization": auth_header, "Content-Type": content_header}
70
71
72
def _check_stash(client, check, verbose=False):
73
    sensu_api = "http://%s:%i" % (SENSU_HOST, SENSU_PORT)
74
    endpoints = [
75
        "silence/%s" % client,
76
        "silence/%s/%s" % (client, check),
77
        "silence/all/%s" % check]
78
79
    for endpoint in endpoints:
80
        url = "%s/stashes/%s" % (sensu_api, endpoint)
81
82
        if verbose:
83
            print('Getting sensu stash info from URL: %s' % url)
84
85
        try:
86
            response = requests.get(url, headers=_get_sensu_request_headers())
87
        except requests.exceptions.ConnectionError:
88
            traceback.print_exc(limit=20)
89
            msg = 'Couldn\'t connect to sensu to get stash info. Is sensu running on %s:%s?' % (
90
                SENSU_HOST, SENSU_PORT
91
            )
92
            raise Exception(msg)
93
94
        if verbose:
95
            print('Sensu response code: %s.' % response.status_code)
96
97
        if response.status_code == 200:
98
            print "Check or client is stashed"
99
            sys.exit(0)
100
101
102
def _get_st2_request_headers():
103
    headers = {}
104
105
    if not UNAUTHED:
106
        if IS_API_KEY_AUTH:
107
            headers[API_KEY_AUTH_HEADER] = ST2_API_KEY
108
        else:
109
            if ST2_AUTH_TOKEN:
110
                headers[TOKEN_AUTH_HEADER] = ST2_AUTH_TOKEN
111
            else:
112
                pass
113
114
    return headers
115
116
117
def _create_trigger_type(verbose=False):
118
    try:
119
        url = _get_st2_triggers_url()
120
        payload = {
121
            'name': ST2_TRIGGERTYPE_NAME,
122
            'pack': ST2_TRIGGERTYPE_PACK,
123
            'description': 'Trigger type for sensu event handler.'
124
        }
125
126
        headers = _get_st2_request_headers()
127
        headers['Content-Type'] = 'application/json; charset=utf-8'
128
129
        if verbose:
130
            print('POST to URL %s for registering trigger. Body = %s, headers = %s.' %
131
                  (url, payload, headers))
132
        post_resp = requests.post(url, data=json.dumps(payload),
133
                                  headers=headers, verify=ST2_SSL_VERFIFY)
134
    except:
135
        traceback.print_exc(limit=20)
136
        raise Exception('Unable to connect to st2 endpoint %s.' % url)
137
    else:
138
        status = post_resp.status_code
139
        if status in UNREACHABLE_CODES:
140
            msg = 'Got response %s. Invalid triggers endpoint %s. Check configuration!' % (
141
                status,
142
                url
143
            )
144
            raise Exception(msg)
145
146
        if status not in OK_CODES:
147
            msg = 'Failed to register trigger type %s.%s with st2. HTTP_CODE: %s' % (
148
                ST2_TRIGGERTYPE_PACK, ST2_TRIGGERTYPE_NAME, status
149
            )
150
            raise Exception(msg)
151
        else:
152
            print('Registered trigger type with st2.')
153
154
155
def _get_auth_url():
156
    return urljoin(ST2_AUTH_BASE_URL, ST2_AUTH_PATH)
157
158
159
def _get_auth_token(verbose=False):
160
    auth_url = _get_auth_url()
161
162
    if verbose:
163
        print('Will POST to URL %s to get auth token.' % auth_url)
164
165
    try:
166
        resp = requests.post(auth_url, json.dumps({'ttl': 5 * 60}),
167
                             auth=(ST2_USERNAME, ST2_PASSWORD), verify=ST2_SSL_VERFIFY)
168
    except:
169
        traceback.print_exc(limit=20)
170
        raise Exception('Unable to connect to st2 endpoint %s.' % auth_url)
171
    else:
172
        if resp.status_code in UNREACHABLE_CODES:
173
            msg = 'Got response %s. Invalid auth endpoint %s. Check configuration!' % (
174
                resp.status_code,
175
                auth_url
176
            )
177
            raise Exception(msg)
178
        if resp.status_code not in OK_CODES:
179
            msg = 'Cannot get a valid auth token from %s. HTTP_CODE: %s' % (
180
                auth_url,
181
                resp.status_code
182
            )
183
            raise Exception(msg)
184
        return resp.json()['token']
185
186
187
def _register_trigger_with_st2(verbose=False):
188
    triggers_url = urljoin(_get_st2_triggers_url(), ST2_TRIGGERTYPE_REF)
189
190
    try:
191
        headers = _get_st2_request_headers()
192
        if verbose:
193
            print('Will GET from URL %s for detecting trigger %s.' % (
194
                  triggers_url, ST2_TRIGGERTYPE_REF))
195
            print('Request headers: %s' % headers)
196
        get_resp = requests.get(triggers_url, headers=headers, verify=ST2_SSL_VERFIFY)
197
198
        if get_resp.status_code != httplib.OK:
199
            _create_trigger_type(verbose=verbose)
200
        else:
201
            body = json.loads(get_resp.text)
202
            if len(body) == 0:
203
                _create_trigger_type(verbose=verbose)
204
    except:
205
        traceback.print_exc(limit=20)
206
        raise Exception('Unable to connect to st2 endpoint %s.' % triggers_url)
207
    else:
208
        if verbose:
209
            print('Successfully registered trigger %s with st2.' % ST2_TRIGGERTYPE_REF)
210
211
212
def _get_st2_triggers_url():
213
    url = urljoin(ST2_API_BASE_URL, ST2_TRIGGERS_PATH)
214
    return url
215
216
217
def _get_st2_webhooks_url():
218
    url = urljoin(ST2_API_BASE_URL, ST2_WEBHOOKS_PATH)
219
    return url
220
221
222
def _post_webhook(url, body, verbose=False):
223
    headers = _get_st2_request_headers()
224
    headers['X-ST2-Integration'] = 'sensu.'
225
    headers['St2-Trace-Tag'] = body['payload']['id']
226
    headers['Content-Type'] = 'application/json; charset=utf-8'
227
228
    try:
229
        if verbose:
230
            print('Webhook POST: url: %s, headers: %s, body: %s\n' % (url, headers, body))
231
        r = requests.post(url, data=json.dumps(body), headers=headers, verify=False)
232
    except:
233
        raise Exception('Cannot connect to st2 endpoint %s.' % url)
234
    else:
235
        status = r.status_code
236
237
        if status in UNREACHABLE_CODES:
238
            msg = 'Webhook URL %s does not exist. Check StackStorm installation!'
239
            raise Exception(msg)
240
241
        if status not in OK_CODES:
242
            sys.stderr.write('Failed posting sensu event to st2. HTTP_CODE: \
243
                %d\n' % status)
244
        else:
245
            sys.stdout.write('Sent sensu event to st2. HTTP_CODE: \
246
                %d\n' % status)
247
248
249
def _post_event_to_st2(payload, verbose=False):
250
    body = {}
251
    body['trigger'] = ST2_TRIGGERTYPE_REF
252
253
    try:
254
        body['payload'] = json.loads(payload)
255
    except:
256
        print('Invalid JSON payload %s.' % payload)
257
        sys.exit(3)
258
259
    try:
260
        client = body['payload']['client']['name']
261
        check = body['payload']['check']['name']
262
    except KeyError:
263
        print('Invalid payload spec %s.' % payload)
264
265
    if not _check_stash(client, check, verbose=verbose):
266
        try:
267
            _post_webhook(url=_get_st2_webhooks_url(), body=body, verbose=verbose)
268
        except:
269
            traceback.print_exc(limit=20)
270
            print('Cannot send event to st2.')
271
            sys.exit(4)
272
273
274
def _register_with_st2(verbose=False):
275
    global REGISTERED_WITH_ST2
276
    try:
277
        if not REGISTERED_WITH_ST2:
278
            if verbose:
279
                print('Checking if trigger %s registered with st2.' % ST2_TRIGGERTYPE_REF)
280
            _register_trigger_with_st2(verbose=verbose)
281
            REGISTERED_WITH_ST2 = True
282
    except:
283
        traceback.print_exc(limit=20)
284
        sys.stderr.write(
285
            'Failed registering with st2. Won\'t post event.\n')
286
        sys.exit(2)
287
288
289
def _set_config_opts(config_file, verbose=False, unauthed=False, ssl_verify=False):
290
    global ST2_USERNAME
291
    global ST2_PASSWORD
292
    global ST2_API_KEY
293
    global ST2_AUTH_TOKEN
294
    global ST2_API_BASE_URL
295
    global ST2_API_BASE_URL
296
    global ST2_AUTH_BASE_URL
297
    global ST2_SSL_VERFIFY
298
    global SENSU_HOST
299
    global SENSU_PORT
300
    global SENSU_USER
301
    global SENSU_PASS
302
    global UNAUTHED
303
    global IS_API_KEY_AUTH
304
305
    UNAUTHED = unauthed
306
    ST2_SSL_VERFIFY = ssl_verify
307
308
    if not os.path.exists(config_file):
309
        print('Configuration file %s not found. Exiting!!!' % config_file)
310
        sys.exit(1)
311
312
    with open(config_file) as f:
313
        config = yaml.safe_load(f)
314
315
        if verbose:
316
            print('Contents of config file: %s' % config)
317
318
        ST2_USERNAME = config['st2_username']
319
        ST2_PASSWORD = config['st2_password']
320
        ST2_API_KEY = config['st2_api_key']
321
        ST2_API_BASE_URL = config['st2_api_base_url']
322
        if not ST2_API_BASE_URL.endswith('/'):
323
            ST2_API_BASE_URL += '/'
324
        ST2_AUTH_BASE_URL = config['st2_auth_base_url']
325
        if not ST2_AUTH_BASE_URL.endswith('/'):
326
            ST2_AUTH_BASE_URL += '/'
327
        SENSU_HOST = config.get('sensu_host', 'localhost')
328
        SENSU_PORT = config.get('sensu_port', '4567')
329
        SENSU_USER = config.get('sensu_user', None)
330
        SENSU_PASS = config.get('sensu_pass', None)
331
332
    if ST2_API_KEY:
333
        IS_API_KEY_AUTH = True
334
335
    if verbose:
336
        print('Unauthed? : %s' % UNAUTHED)
337
        print('API key auth?: %s' % IS_API_KEY_AUTH)
338
        print('SSL_VERIFY? : %s' % ST2_SSL_VERFIFY)
339
340
    if not UNAUTHED and not IS_API_KEY_AUTH:
341
        try:
342
            if not ST2_AUTH_TOKEN:
343
                if verbose:
344
                    print('No auth token found. Let\'s get one from StackStorm!')
345
                ST2_AUTH_TOKEN = _get_auth_token(verbose=verbose)
346
        except:
347
            traceback.print_exc(limit=20)
348
            print('Unable to negotiate an auth token. Exiting!')
349
            sys.exit(1)
350
351
352
def main(config_file, payload, verbose=False, unauthed=False, ssl_verify=False):
353
    _set_config_opts(config_file=config_file, unauthed=unauthed, verbose=verbose,
354
                     ssl_verify=ssl_verify)
355
    _register_with_st2(verbose=verbose)
356
    _post_event_to_st2(payload, verbose=verbose)
357
358
359
if __name__ == '__main__':
360
    parser = argparse.ArgumentParser(description='StackStorm sensu event handler.')
361
    parser.add_argument('config_path',
362
                        help='Exchange to listen on')
363
    parser.add_argument('--verbose', '-v', required=False, action='store_true',
364
                        help='Verbose mode.')
365
    parser.add_argument('--unauthed', '-u', required=False, action='store_true',
366
                        help='Allow to post to unauthed st2. E.g. when auth is disabled ' +
367
                        'server side.')
368
    parser.add_argument('--ssl-verify', '-k', required=False, action='store_true',
369
                        help='Turn on SSL verification for st2 APIs.')
370
    args = parser.parse_args()
371
    payload = sys.stdin.read().strip()
372
    main(config_file=args.config_path, payload=payload, verbose=args.verbose,
373
         unauthed=args.unauthed, ssl_verify=args.ssl_verify)
374