Refactoring: Moved check parameters from unsorted.py to dedicated modules (CMK-1393)
[check_mk.git] / cmk_base / check_api.py
blobe5ea7c08697beee8774edde412aa29f106ebbf2c
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 """
28 The things in this module specify the official Check_MK check API. Meaning all
29 variables, functions etc. and default modules that are available to checks.
31 Modules available by default (pre imported by Check_MK):
32 collections
33 enum
34 fnmatch
35 functools
36 math
39 socket
40 sys
41 time
42 pprint
44 Global variables:
45 from cmk.utils.regex import regex
46 import cmk.utils.render as render
47 core_state_names Names of states. Usually used to convert numeric states
48 to their name for adding it to the plugin output.
49 The mapping is like this:
51 -1: 'PEND'
52 0: 'OK'
53 1: 'WARN'
54 2: 'CRIT'
55 3: 'UNKN'
57 state_markers Symbolic representations of states in plugin output.
58 Will be displayed colored by the Check_MK GUI.
59 The mapping is like this:
61 0: ''
62 1: '(!)'
63 2: '(!!)'
64 3: '(?)'
66 nagios_illegal_chars Characters not allowed to be used in service
67 descriptions. Can be used in discovery functions to
68 remove unwanted characters from a string. The unwanted
69 chars default are: `;~!$%^&*|\'"<>?,()=
72 OID_BIN TODO
73 OID_END TODO
74 OID_END_BIN TODO
75 OID_END_OCTET_STRING TODO
76 OID_STRING TODO
78 MGMT_ONLY Check is only executed for management boards.
79 HOST_PRECEDENCE Use host address/credentials eg. when it's a SNMP HOST.
80 HOST_ONLY Check is only executed for real SNMP hosts.
82 RAISE Used as value for the "onwrap" argument of the get_rate()
83 function. See get_rate() documentation for details
84 SKIP Used as value for the "onwrap" argument of the get_rate()
85 function. See get_rate() documentation for details
86 ZERO Used as value for the "onwrap" argument of the get_rate()
87 function. See get_rate() documentation for details
88 """ # # pylint: disable=pointless-string-statement
90 # NOTE: The above suppression is necessary because our testing framework blindly
91 # concatenates lots of files, including this one.
93 # We import several modules here for the checks
94 # pylint: disable=unused-import
96 # TODO: Move imports directly to checks?
97 import collections
98 import enum
99 import fnmatch
100 import functools
101 import math
102 import os
103 import re
104 import socket
105 import sys
106 import time
107 # NOTE: We do not use pprint in this module, but it is part of the check API.
108 import pprint # pylint: disable=unused-import
110 from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union # pylint: disable=unused-import
112 import six
114 import cmk.utils.debug as _debug
115 import cmk.utils.defines as _defines
116 import cmk.utils.paths as _paths
117 from cmk.utils.exceptions import MKGeneralException
118 from cmk.utils.regex import regex
119 import cmk.utils.render as render
121 # These imports are not meant for use in the API. So we prefix the names
122 # with an underscore. These names will be skipped when loading into the
123 # check context.
124 import cmk_base.utils as _utils
125 import cmk_base.config as _config
126 import cmk_base.console as _console
127 import cmk_base.snmp_utils as _snmp_utils
128 import cmk_base.item_state as _item_state
129 import cmk_base.prediction as _prediction
130 import cmk_base.check_api_utils as _check_api_utils
133 def get_check_api_context():
134 """This is called from cmk_base code to get the Check API things. Don't
135 use this from checks."""
136 return {k: v for k, v in globals().items() if not k.startswith("_")}
140 # .--Check API-----------------------------------------------------------.
141 # | ____ _ _ _ ____ ___ |
142 # | / ___| |__ ___ ___| | __ / \ | _ \_ _| |
143 # | | | | '_ \ / _ \/ __| |/ / / _ \ | |_) | | |
144 # | | |___| | | | __/ (__| < / ___ \| __/| | |
145 # | \____|_| |_|\___|\___|_|\_\ /_/ \_\_| |___| |
146 # | |
147 # +----------------------------------------------------------------------+
148 # | Helper API for being used in checks |
149 # '----------------------------------------------------------------------'
151 # Names of texts usually output by checks
152 core_state_names = _defines.short_service_state_names()
154 # Symbolic representations of states in plugin output
155 state_markers = _check_api_utils.state_markers
157 BINARY = _snmp_utils.BINARY
158 CACHED_OID = _snmp_utils.CACHED_OID
160 OID_END = _snmp_utils.OID_END
161 OID_STRING = _snmp_utils.OID_STRING
162 OID_BIN = _snmp_utils.OID_BIN
163 OID_END_BIN = _snmp_utils.OID_END_BIN
164 OID_END_OCTET_STRING = _snmp_utils.OID_END_OCTET_STRING
165 binstring_to_int = _snmp_utils.binstring_to_int
167 # Management board checks
168 MGMT_ONLY = _check_api_utils.MGMT_ONLY # Use host address/credentials when it's a SNMP HOST
169 HOST_PRECEDENCE = _check_api_utils.HOST_PRECEDENCE # Check is only executed for mgmt board (e.g. Managegment Uptime)
170 HOST_ONLY = _check_api_utils.HOST_ONLY # Check is only executed for real SNMP host (e.g. interfaces)
172 host_name = _check_api_utils.host_name
173 service_description = _check_api_utils.service_description
174 check_type = _check_api_utils.check_type
177 def saveint(i):
178 """Tries to cast a string to an integer and return it. In case this
179 fails, it returns 0.
181 Advice: Please don't use this function in new code. It is understood as
182 bad style these days, because in case you get 0 back from this function,
183 you can not know whether it is really 0 or something went wrong."""
184 try:
185 return int(i)
186 except:
187 return 0
190 def savefloat(f):
191 """Tries to cast a string to an float and return it. In case this fails,
192 it returns 0.0.
194 Advice: Please don't use this function in new code. It is understood as
195 bad style these days, because in case you get 0.0 back from this function,
196 you can not know whether it is really 0.0 or something went wrong."""
197 try:
198 return float(f)
199 except:
200 return 0.0
203 # The function no_discovery_possible is as stub function used for
204 # those checks that do not support inventory. It must be known before
205 # we read in all the checks
207 # TODO: This seems to be an old part of the check API and not used for
208 # a long time. Deprecate this as part of the and move it to the
209 # cmk_base.config module.
210 no_discovery_possible = _check_api_utils.no_discovery_possible
212 service_extra_conf = _config.service_extra_conf
213 host_extra_conf = _config.host_extra_conf
214 in_binary_hostlist = _config.in_binary_hostlist
215 in_extraconf_hostlist = _config.in_extraconf_hostlist
216 hosttags_match_taglist = _config.hosttags_match_taglist
217 host_extra_conf_merged = _config.host_extra_conf_merged
218 get_rule_options = _config.get_rule_options
219 all_matching_hosts = _config.all_matching_hosts
221 tags_of_host = _config.tags_of_host
222 nagios_illegal_chars = _config.nagios_illegal_chars
223 is_ipv6_primary = _config.is_ipv6_primary
224 is_cmc = _config.is_cmc
226 get_age_human_readable = lambda secs: str(render.Age(secs))
227 get_bytes_human_readable = render.fmt_bytes
228 quote_shell_string = _utils.quote_shell_string
231 def get_checkgroup_parameters(group, deflt=None):
232 return _config.checkgroup_parameters.get(group, deflt)
235 # TODO: Replace by some render.* function / move to render module?
236 def get_filesize_human_readable(size):
237 """Format size of a file for humans.
239 Similar to get_bytes_human_readable, but optimized for file
240 sizes. Really only use this for files. We assume that for smaller
241 files one wants to compare the exact bytes of a file, so the
242 threshold to show the value as MB/GB is higher as the one of
243 get_bytes_human_readable()."""
244 if size < 4 * 1024 * 1024:
245 return "%d B" % int(size)
246 elif size < 4 * 1024 * 1024 * 1024:
247 return "%.2f MB" % (float(size) / (1024 * 1024))
248 return "%.2f GB" % (float(size) / (1024 * 1024 * 1024))
251 # TODO: Replace by some render.* function / move to render module?
252 def get_nic_speed_human_readable(speed):
253 """Format network speed (bit/s) for humans."""
254 try:
255 speedi = int(speed)
256 if speedi == 10000000:
257 speed = "10 Mbit/s"
258 elif speedi == 100000000:
259 speed = "100 Mbit/s"
260 elif speedi == 1000000000:
261 speed = "1 Gbit/s"
262 elif speedi < 1500:
263 speed = "%d bit/s" % speedi
264 elif speedi < 1000000:
265 speed = "%.1f Kbit/s" % (speedi / 1000.0)
266 elif speedi < 1000000000:
267 speed = "%.2f Mbit/s" % (speedi / 1000000.0)
268 else:
269 speed = "%.2f Gbit/s" % (speedi / 1000000000.0)
270 except:
271 pass
272 return speed
275 # TODO: Replace by some render.* function / move to render module?
276 def get_timestamp_human_readable(timestamp):
277 """Format a time stamp for humans in "%Y-%m-%d %H:%M:%S" format.
278 In case None is given or timestamp is 0, it returns "never"."""
279 if timestamp:
280 return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(float(timestamp)))
281 return "never"
284 # TODO: Replace by some render.* function / move to render module?
285 def get_relative_date_human_readable(timestamp):
286 """Formats the given timestamp for humans "in ..." for future times
287 or "... ago" for past timestamps."""
288 now = time.time()
289 if timestamp > now:
290 return "in " + get_age_human_readable(timestamp - now)
291 return get_age_human_readable(now - timestamp) + " ago"
294 # TODO: Replace by some render.* function / move to render module?
295 def get_percent_human_readable(perc, precision=2):
296 """Format perc (0 <= perc <= 100 + x) so that precision
297 digits are being displayed. This avoids a "0.00%" for
298 very small numbers."""
299 if perc > 0:
300 perc_precision = max(1, 2 - int(round(math.log(perc, 10))))
301 else:
302 perc_precision = 1
303 return "%%.%df%%%%" % perc_precision % perc
307 # Counter handling
310 set_item_state = _item_state.set_item_state
311 get_item_state = _item_state.get_item_state
312 get_all_item_states = _item_state.get_all_item_states
313 clear_item_state = _item_state.clear_item_state
314 clear_item_states_by_full_keys = _item_state.clear_item_states_by_full_keys
315 get_rate = _item_state.get_rate
316 get_average = _item_state.get_average
317 # TODO: Cleanup checks and deprecate this
318 last_counter_wrap = _item_state.last_counter_wrap
320 SKIP = _item_state.SKIP
321 RAISE = _item_state.RAISE
322 ZERO = _item_state.ZERO
324 MKCounterWrapped = _item_state.MKCounterWrapped
327 def _normalize_bounds(levels):
328 if len(levels) == 2: # upper warn and crit
329 warn_upper, crit_upper = levels[0], levels[1]
330 warn_lower, crit_lower = None, None
332 else: # upper and lower warn and crit
333 warn_upper, crit_upper = levels[0], levels[1]
334 warn_lower, crit_lower = levels[2], levels[3]
336 return warn_upper, crit_upper, warn_lower, crit_lower
339 def _check_boundaries(value, levels, human_readable_func=str):
340 def levelsinfo_ty(ty, warn, crit, human_readable_func):
341 if human_readable_func is None:
342 human_readable_func = str
343 return " (warn/crit %s %s/%s)" % (ty, human_readable_func(warn), human_readable_func(crit))
345 warn_upper, crit_upper, warn_lower, crit_lower = _normalize_bounds(levels)
346 # Critical cases
347 if crit_upper is not None and value >= crit_upper:
348 return 2, levelsinfo_ty("at", warn_upper, crit_upper, human_readable_func)
349 if crit_lower is not None and value < crit_lower:
350 return 2, levelsinfo_ty("below", warn_lower, crit_lower, human_readable_func)
352 # Warning cases
353 if warn_upper is not None and value >= warn_upper:
354 return 1, levelsinfo_ty("at", warn_upper, crit_upper, human_readable_func)
355 if warn_lower is not None and value < warn_lower:
356 return 1, levelsinfo_ty("below", warn_lower, crit_lower, human_readable_func)
357 return 0, ""
360 def check_levels(value,
361 dsname,
362 params,
363 unit="",
364 factor=1.0,
365 scale=1.0,
366 statemarkers=False,
367 human_readable_func=None,
368 infoname=None):
369 """Generic function for checking a value against levels
371 This also supports predictive levels.
373 value: currently measured value
374 dsname: name of the datasource in the RRD that corresponds to this value
375 unit: unit to be displayed in the plugin output, e.g. "MB/s"
376 factor: the levels are multiplied with this factor before applying
377 them to the value. This is being used for the CPU load check
378 currently. The levels here are "per CPU", so the number of
379 CPUs is used as factor.
380 scale: Scale of the levels in relation to "value" and the value in the RRDs.
381 For example if the levels are specified in GB and the RRD store KB, then
382 the scale is 1024*1024.
383 human_readable_func: Single argument function to present in a human readable fashion
384 the value. It has priority over the unit argument.
385 infoname: Perf value name for infotext, defaults to dsname
387 if unit not in ('', '%'):
388 unit = " " + unit # Insert space before MB, GB, etc.
390 if human_readable_func is None:
391 human_readable_func = lambda x: "%.2f%s" % (x / scale, unit)
393 def scale_value(v):
394 if v is None:
395 return None
397 return v * factor * scale
399 infotext = "%s: %s" % (infoname or dsname, human_readable_func(value))
400 perf_value = (dsname, value)
401 # None, {} or (None, None) -> do not check any levels
402 if not params or params == (None, None):
403 return 0, infotext, [perf_value]
405 # Pair of numbers -> static levels
406 elif isinstance(params, tuple):
407 levels = map(scale_value, _normalize_bounds(params))
408 ref_value = None
410 # Dictionary -> predictive levels
411 else:
412 try:
413 ref_value, levels = \
414 _prediction.get_levels(host_name(), service_description(),
415 dsname, params, "MAX", levels_factor=factor * scale)
417 if ref_value:
418 predictive_levels_msg = "predicted reference: %s" % human_readable_func(ref_value)
419 else:
420 predictive_levels_msg = "no reference for prediction yet"
422 except MKGeneralException as e:
423 ref_value = None
424 levels = [None, None, None, None]
425 predictive_levels_msg = "no reference for prediction (%s)" % e
427 except Exception as e:
428 if _debug.enabled():
429 raise
430 return 3, "%s" % e, []
432 if predictive_levels_msg:
433 infotext += " (%s)" % predictive_levels_msg
435 state, levelstext = _check_boundaries(value, levels, human_readable_func)
437 infotext += levelstext
439 if statemarkers:
440 infotext += state_markers[state]
442 perfdata = [perf_value + tuple(levels[:2])]
443 if ref_value:
444 perfdata.append(('predict_' + dsname, ref_value))
446 return state, infotext, perfdata
449 def get_effective_service_level():
450 """Get the service level that applies to the current service.
451 This can only be used within check functions, not during discovery nor parsing."""
452 service_levels = _config.service_extra_conf(host_name(), service_description(),
453 _config.service_service_levels)
455 if service_levels:
456 return service_levels[0]
457 else:
458 service_levels = _config.host_extra_conf(host_name(), _config.host_service_levels)
459 if service_levels:
460 return service_levels[0]
461 return 0
464 def utc_mktime(time_struct):
465 """Works like time.mktime() but assumes the time_struct to be in UTC,
466 not in local time."""
467 import calendar
468 return calendar.timegm(time_struct)
471 def passwordstore_get_cmdline(fmt, pw):
472 """Use this to prepare a command line argument for using a password from the
473 Check_MK password store or an explicitly configured password."""
474 if not isinstance(pw, tuple):
475 pw = ("password", pw)
477 if pw[0] == "password":
478 return fmt % pw[1]
480 return ("store", pw[1], fmt)
483 def get_http_proxy(http_proxy):
484 """Returns proxy URL to be used for HTTP requests
486 Pass a value configured by the user using the HTTPProxyReference valuespec to this function
487 and you will get back ether a proxy URL, an empty string to enforce no proxy usage or None
488 to use the proxy configuration from the process environment.
490 return _config.get_http_proxy(http_proxy)
493 def get_agent_data_time():
494 """Use this function to get the age of the agent data cache file
495 of tcp or snmp hosts or None in case of piggyback data because
496 we do not exactly know the latest agent data. Maybe one time
497 we can handle this. For cluster hosts an exception is raised."""
498 return _agent_cache_file_age(host_name(), check_type())
501 def _agent_cache_file_age(hostname, check_plugin_name):
502 if _config.is_cluster(hostname):
503 raise MKGeneralException("get_agent_data_time() not valid for cluster")
505 import cmk_base.check_utils
506 if cmk_base.check_utils.is_snmp_check(check_plugin_name):
507 cachefile = _paths.tcp_cache_dir + "/" + hostname + "." + check_plugin_name.split(".")[0]
508 elif cmk_base.check_utils.is_tcp_check(check_plugin_name):
509 cachefile = _paths.tcp_cache_dir + "/" + hostname
510 else:
511 cachefile = None
513 if cachefile is not None and os.path.exists(cachefile):
514 return _utils.cachefile_age(cachefile)
516 return None
519 def get_parsed_item_data(check_function):
520 """Use this decorator to determine the parsed item data outside
521 of the respective check function.
523 The check function can hence be defined as follows:
525 @get_parsed_item_data
526 def check_<check_name>(item, params, data):
529 In case of parsed not being a dict the decorator returns 3
530 (UNKN state) with a wrong usage message.
531 In case of item not existing as a key in parsed or parsed[item]
532 not existing the decorator gives an empty return leading to
533 cmk_base returning 3 (UNKN state) with an item not found message
534 (see cmk_base/checking.py).
537 @functools.wraps(check_function)
538 def wrapped_check_function(item, params, parsed):
539 if not isinstance(parsed, dict):
540 return 3, "Wrong usage of decorator function 'get_parsed_item_data': parsed is not a dict"
541 if item not in parsed or not parsed[item]:
542 return
543 return check_function(item, params, parsed[item])
545 return wrapped_check_function
548 def discover_single(info):
549 # type: (Union[List, Dict]) -> Optional[List]
550 """Return a discovered item in case there is info text or parsed"""
551 if info:
552 return [(None, {})]
553 return None
556 def validate_filter(filter_function):
557 # type: (Callable) -> Callable
558 """Validate function argument is a callable and return it"""
560 if callable(filter_function):
561 return filter_function
562 elif filter_function is not None:
563 raise ValueError("Filtering function is not a callable,"
564 " a {} has been given.".format(type(filter_function)))
565 return lambda *entry: entry[0]
568 def discover(selector=None, default_params=None):
569 # type (Callable, Union[dict, str]) -> Callable
570 """Helper function to assist with service discoveries
572 The discovery function is in many cases just a boilerplate function to
573 recognize services listed in your parsed dictionary or the info
574 list. It in general looks like
576 def inventory_check(parsed):
577 for key, value in parsed.items():
578 if some_condition_based_on(key, value):
579 yield key, parameters
582 The idea of this helper is to allow you only to worry about the logic
583 function that decides if an entry is a service to be discovered or not.
586 Keyword Arguments:
587 selector -- Filtering function (default lambda entry: entry[0])
588 Default: Uses the key or first item of info variable
589 default_params -- Default parameters for discovered items (default {})
591 Possible uses:
593 If your discovery function recognizes every entry of your parsed
594 dictionary or every row of the info list as a service, then you
595 just need to call discover().
597 check_info["chk"] = {'inventory_function': discover()}
599 In case you want to have a simple filter function when dealing with
600 the info list, you can directly give a lambda function. If this
601 function returns a Boolean the first item of every entry is taken
602 as the service name, if the function returns a string that will be
603 taken as the service name. For this example we discover as services
604 entries where item3 is positive and name the service according to
605 item2.
607 check_info["chk"] = {'inventory_function': discover(selector=lambda line: line[2] if line[3]>0 else False)}
609 In case you have a more complicated selector condition and also
610 want to include default parameters you may use a decorator.
612 Please note: that this discovery function does not work with the
613 whole parsed/info data but only implements the logic for selecting
614 each individual entry as a service.
616 In the next example, we will process each entry of the parsed data
617 dictionary. Use as service name the capitalized key when the
618 corresponding value has certain keywords.
620 @discover(default_params="the_check_default_levels")
621 def inventory_thecheck(key, value):
622 required_entries = ["used", "ready", "total", "uptime"]
623 if all(data in value for data in required_entries):
624 return key.upper()
626 check_info["chk"] = {'inventory_function': inventory_thecheck}
629 def roller(parsed):
630 if isinstance(parsed, dict):
631 return parsed.iteritems()
632 elif isinstance(parsed, (list, tuple)):
633 return parsed
634 raise ValueError("Discovery function only works with dictionaries,"
635 " lists, and tuples you gave a {}".format(type(parsed)))
637 def _discovery(filter_function):
638 # type (Callable) -> Callable
639 @functools.wraps(filter_function)
640 def discoverer(parsed):
641 # type (Union[dict,list]) -> Iterable[Tuple]
643 params = default_params if isinstance(default_params,
644 six.string_types + (dict,)) else {}
645 filterer = validate_filter(filter_function)
646 from_dict = isinstance(parsed, dict)
648 for entry in roller(parsed):
649 if from_dict:
650 key, value = entry
651 name = filterer(key, value)
652 else:
653 name = filterer(entry)
655 if isinstance(name, six.string_types):
656 yield (name, params)
657 elif name is True and from_dict:
658 yield (key, params)
659 elif name is True and not from_dict:
660 yield (entry[0], params)
661 elif name and hasattr(name, '__iter__'):
662 for new_name in name:
663 yield (new_name, params)
665 return discoverer
667 if callable(selector):
668 return _discovery(selector)
670 if selector is None and default_params is None:
671 return _discovery(lambda *args: args[0])
673 return _discovery
676 # NOTE: Currently this is not really needed, it is just here to keep any start
677 # import in sync with our intended API.
678 # TODO: Do we really need this? Is there code which uses a star import for this
679 # module?
680 __all__ = get_check_api_context().keys()