GUI CSS: Removed snapin styles from py modules and added a _snapins.scss for the...
[check_mk.git] / active_checks / check_form_submit
blob00a5ec4545b1e4b2922e4af019e2c4540bbd765e
1 #!/usr/bin/env python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 # This check performs HTTP requests with some advanced features like
28 # a) Detecting, populating and submitting HTML forms
29 # b) Accepts and uses cookies
30 # c) Follos HTTP redirects
31 # d) Extends HTTP headers
33 # Example calls:
35 # Call the page test.php, find the single form on that page and
36 # submit it with default values:
38 # ./check_form_submit -I localhost -u /test.php
40 # Same as above, but expect the string "Hello" in the response
41 # of the form:
43 # ./check_form_submit -I localhost -u /test.php -e "Hello"
45 # Login as omdadmin with password omd, in the OMD site named /event,
46 # let the login redirect to the wato.py and expect the string WATO
47 # in this response:
49 # ./check_form_submit -I localhost -u /event -q '_origtarget=wato.py&_username=omdadmin&_password=omd' -e 'WATO'
51 import cookielib
52 import getopt
53 import re
54 import socket
55 import sys
56 import urllib
57 import urllib2
58 from HTMLParser import HTMLParser
61 def usage():
62 sys.stderr.write("""
63 USAGE: check_form_submit -I <HOSTADDRESS> [-u <URI>] [-p <PORT>] [-s]
64 [-f <FORMNAME>] [-q <QUERYPARAMS>] [-e <REGEX>] [-t <TIMEOUT> ]
65 [-n <WARN>,<CRIT>]
67 OPTIONS:
68 -I HOSTADDRESS The IP address or hostname of the host to contact.
69 This option can be given multiple times to contact
70 several hosts with the same query.
71 -u URI The URL string to query, "/" by default
72 -p PORT TCP Port to communicate with
73 -s Encrypt the connection using SSL
74 -f FORMNAME Name of the form to fill, must match with the
75 contents of the "name" attribute
76 -q QUERYPARAMS Keys/Values of form fields to be popuplated
77 -e REGEX Expected regular expression in the HTML response
78 after form submission
79 -t TIMEOUT HTTP connect timeout in seconds
80 -n WARN,CRIT Is only used in "multiple" mode. Number of successful
81 responses to result in a WARN and/or CRIT state
82 -d Enable debug mode
83 -h, --help Show this help message and exit
85 """)
88 def debug(msg):
89 if opt_debug:
90 sys.stderr.write('%s\n' % msg)
93 class HostResult(Exception):
94 def __init__(self, result):
95 super(HostResult, self).__init__()
96 self.result = result
99 def new_state(rc, s):
100 raise HostResult((rc, s))
103 def bail_out(rc, s):
104 stxt = ['OK', 'WARN', 'CRIT', 'UNKN'][rc]
105 sys.stdout.write('%s - %s\n' % (stxt, s))
106 sys.exit(rc)
109 def get_base_url(ssl, host, port):
110 if not ssl and port == 443:
111 ssl = True
113 proto = 'https' if ssl else 'http'
114 if (proto == 'http' and port == 80) or (proto == 'https' and port == 443):
115 portspec = ''
116 else:
117 portspec = ':%d' % port
119 return '%s://%s%s' % (proto, host, portspec)
122 # TODO: Refactor to requests
123 def init_http():
124 return urllib2.build_opener(urllib2.HTTPRedirectHandler(), urllib2.HTTPHandler(debuglevel=0),
125 urllib2.HTTPSHandler(debuglevel=0),
126 urllib2.HTTPCookieProcessor(cookielib.CookieJar()))
129 def open_url(client, url, method='GET', data=None, timeout=None):
130 if method == 'GET' and data is not None:
131 # Add the query string to the url in this case
132 start = '&' if '?' in url else '?'
133 url += start + urllib.urlencode(data)
134 data = None
136 try:
137 if data:
138 debug('POST %s' % url)
139 data = urllib.urlencode(data.items())
140 debug(' => %s' % data)
141 fd = client.open(url, data, timeout) # will be a POST
142 else:
143 debug('GET %s' % url)
144 fd = client.open(url, timeout=timeout) # GET
145 except urllib2.HTTPError, e:
146 new_state(2, 'Unable to open %s: [%d] %s' % (url, e.code, e))
147 except urllib2.URLError, e:
148 new_state(2, 'Unable to open %s: %s' % (url, e.reason))
149 except socket.timeout, e:
150 new_state(2, 'Unable to open %s: %s' % (url, e))
151 real_url = fd.geturl()
152 code = fd.getcode()
153 content = fd.read()
155 encoding = fd.headers.getparam('charset')
156 if encoding:
157 content = content.decode(encoding)
159 debug('CODE: %s RESPONSE:' % code)
160 debug('\n'.join(
161 [' %02d %s' % (index + 1, line) for index, line in enumerate(content.split('\n'))]))
162 return code, real_url, content
165 class FormParser(HTMLParser):
166 def __init__(self):
167 self.forms = {}
168 self.current_form = None
169 HTMLParser.__init__(self)
171 def handle_starttag(self, tag, attrs):
172 attrs = dict(attrs)
174 if tag == 'form':
175 name = attrs.get('name', 'unnamed-%d' % (len(self.forms) + 1))
176 self.forms[name] = {
177 'attrs': dict(attrs),
178 'elements': {},
180 self.current_form = self.forms[name]
181 elif tag == 'input':
182 if self.current_form is None:
183 debug('Ignoring form field out of form tag')
184 else:
185 if 'name' in attrs:
186 self.current_form['elements'][attrs['name']] = attrs.get('value', '')
187 else:
188 debug('Ignoring form field without name %r' % attrs)
190 def handle_endtag(self, tag):
191 if tag == 'form':
192 self.current_form = None
195 # Parse XML to find all form elements
196 # One form found and no form_name given, use that one
197 # Loop all forms for the given form_name, use the matching one
198 # otherwise raise an exception
199 def parse_form(content, form_name):
200 parser = FormParser()
201 parser.feed(content)
202 forms = parser.forms
203 num_forms = len(forms)
205 if num_forms == 0:
206 new_state(2, 'Found no form element in HTML code')
208 elif num_forms == 1 and form_name is not None and form_name in forms:
209 new_state(
211 'Found one form with name "%s" but expected name "%s"' % (forms.keys()[0], form_name))
213 elif num_forms == 1:
214 form = forms[forms.keys()[0]]
216 elif form_name is None:
217 new_state(
218 2, 'No form name provided but found multiple forms (Names: %s)' % (', '.join(
219 forms.keys())))
221 else:
222 form = forms.get(form_name)
223 if form is None:
224 new_state(
225 2, 'Found no form with name "%s" (Available: %s)' % (form_name, ', '.join(
226 forms.keys())))
228 return form
231 def update_form_vars(form_elem, params):
232 v = form_elem['elements'].copy()
233 v.update(params)
234 return v
237 opt_debug = False
240 def main():
241 global opt_debug
242 short_options = 'I:u:p:H:f:q:e:t:n:sd'
243 long_options = ["help"]
245 try:
246 opts = getopt.getopt(sys.argv[1:], short_options, long_options)[0]
247 except getopt.GetoptError, err:
248 sys.stderr.write("%s\n" % err)
249 sys.exit(1)
251 hosts = []
252 multiple = False
253 uri = '/'
254 port = 80
255 ssl = False
256 form_name = None
257 params = {}
258 expect_regex = None
259 timeout = 10 # seconds
260 num_warn = None
261 num_crit = None
263 for o, a in opts:
264 if o in ['-h', '--help']:
265 usage()
266 sys.exit(0)
267 elif o == '-I':
268 hosts.append(a)
269 elif o == '-u':
270 uri = a
271 elif o == '-p':
272 port = int(a)
273 elif o == '-s':
274 ssl = True
275 elif o == '-f':
276 form_name = a
277 elif o == '-q':
278 params = dict([parts.split('=', 1) for parts in a.split('&')])
279 elif o == '-e':
280 expect_regex = a
281 elif o == '-t':
282 timeout = int(a)
283 elif o == '-n':
284 if ',' in a:
285 num_warn, num_crit = map(int, a.split(',', 1))
286 else:
287 num_warn = int(a)
288 elif o == '-d':
289 opt_debug = True
291 if not hosts:
292 sys.stderr.write('Please provide the host to query via -I <HOSTADDRESS>.\n')
293 usage()
294 sys.exit(1)
296 if len(hosts) > 1:
297 multiple = True
299 try:
300 client = init_http()
302 states = {}
303 for host in hosts:
304 base_url = get_base_url(ssl, host, port)
305 try:
306 # Perform first HTTP request to fetch the page containing the form(s)
307 _code, real_url, body = open_url(client, base_url + uri, timeout=timeout)
309 form = parse_form(body, form_name)
311 # Get all fields and prefilled values from that form
312 # Put the values of the given query params in these forms
313 form_vars = update_form_vars(form, params)
315 # Issue a HTTP request with those parameters
316 # Extract the form target and method
317 method = form['attrs'].get('method', 'GET').upper()
318 target = form['attrs'].get('action', real_url)
319 if target[0] == '/':
320 # target is given as absolute path, relative to hostname
321 target = base_url + target
322 elif target[0] != '':
323 # relative URL
324 target = '%s/%s' % ('/'.join(real_url.rstrip('/').split('/')[:-1]), target)
326 _code, real_url, content = open_url(
327 client, target, method, form_vars, timeout=timeout)
329 # If a expect_regex is given, check wether or not it is present in the response
330 if expect_regex is not None:
331 matches = re.search(expect_regex, content)
332 if matches is not None:
333 new_state(0, 'Found expected regex "%s" in form response' % expect_regex)
334 else:
335 new_state(
336 2, 'Expected regex "%s" could not be found in form response' %
337 expect_regex)
338 else:
339 new_state(0, 'Form has been submitted')
340 except HostResult, e:
341 if not multiple:
342 bail_out(*e.result)
343 states[host] = e.result
345 if multiple:
346 failed = [pair for pair in states.items() if pair[1][0] != 0]
347 success = [pair for pair in states.items() if pair[1][0] == 0]
348 max_state = max([state for state, _output in states.values()])
350 sum_state = 0
351 if num_warn is None and num_crit is None:
352 # use the worst state as summary state
353 sum_state = max_state
355 elif num_crit is not None and len(success) <= num_crit:
356 sum_state = 2
358 elif num_warn is not None and len(success) <= num_warn:
359 sum_state = 1
361 txt = '%d succeeded, %d failed' % (len(success), len(failed))
362 if failed:
363 txt += ' (%s)' % ', '.join(['%s: %s' % (n, msg[1]) for n, msg in failed])
364 bail_out(sum_state, txt)
366 except Exception, e:
367 if opt_debug:
368 raise
369 bail_out(3, 'Exception occured: %s\n' % e)
372 if __name__ == "__main__":
373 main()