2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
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.
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):
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:
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:
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: `;~!$%^&*|\'"<>?,()=
75 OID_END_OCTET_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?
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
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
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 # | \____|_| |_|\___|\___|_|\_\ /_/ \_\_| |___| |
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
178 """Tries to cast a string to an integer and return it. In case this
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."""
191 """Tries to cast a string to an float and return it. In case this fails,
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."""
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."""
256 if speedi
== 10000000:
258 elif speedi
== 100000000:
260 elif speedi
== 1000000000:
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)
269 speed
= "%.2f Gbit/s" % (speedi
/ 1000000000.0)
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"."""
280 return time
.strftime("%Y-%m-%d %H:%M:%S", time
.localtime(float(timestamp
)))
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."""
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."""
300 perc_precision
= max(1, 2 - int(round(math
.log(perc
, 10))))
303 return "%%.%df%%%%" % perc_precision
% perc
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
)
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
)
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
)
360 def check_levels(value
,
367 human_readable_func
=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
)
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
))
410 # Dictionary -> predictive levels
413 ref_value
, levels
= \
414 _prediction
.get_levels(host_name(), service_description(),
415 dsname
, params
, "MAX", levels_factor
=factor
* scale
)
418 predictive_levels_msg
= "predicted reference: %s" % human_readable_func(ref_value
)
420 predictive_levels_msg
= "no reference for prediction yet"
422 except MKGeneralException
as e
:
424 levels
= [None, None, None, None]
425 predictive_levels_msg
= "no reference for prediction (%s)" % e
427 except Exception as e
:
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
440 infotext
+= state_markers
[state
]
442 perfdata
= [perf_value
+ tuple(levels
[:2])]
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
)
456 return service_levels
[0]
458 service_levels
= _config
.host_extra_conf(host_name(), _config
.host_service_levels
)
460 return service_levels
[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."""
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":
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
513 if cachefile
is not None and os
.path
.exists(cachefile
):
514 return _utils
.cachefile_age(cachefile
)
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
]:
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"""
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.
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 {})
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
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):
626 check_info["chk"] = {'inventory_function': inventory_thecheck}
630 if isinstance(parsed
, dict):
631 return parsed
.iteritems()
632 elif isinstance(parsed
, (list, tuple)):
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
):
651 name
= filterer(key
, value
)
653 name
= filterer(entry
)
655 if isinstance(name
, six
.string_types
):
657 elif name
is True and from_dict
:
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
)
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])
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
680 __all__
= get_check_api_context().keys()