Refactoring: Moved check parameters from unsorted.py to dedicated modules (CMK-1393)
[check_mk.git] / cmk / gui / metrics.py
blobe2c8844ee5f5a5837b56e5e70afabfe3f122a507
1 #!/usr/bin/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 # Frequently used variable names:
28 # perf_data_string: Raw performance data as sent by the core, e.g "foo=17M;1;2;4;5"
29 # perf_data: Split performance data, e.g. [("foo", "17", "M", "1", "2", "4", "5")]
30 # translated_metrics: Completely parsed and translated into metrics, e.g. { "foo" : { "value" : 17.0, "unit" : { "render" : ... }, ... } }
31 # color: RGB color representation ala HTML, e.g. "#ffbbc3" or "#FFBBC3", len() is always 7!
32 # color_rgb: RGB color split into triple (r, g, b), where r,b,g in (0.0 .. 1.0)
33 # unit_name: The ID of a unit, e.g. "%"
34 # unit: The definition-dict of a unit like in unit_info
35 # graph_template: Template for a graph. Essentially a dict with the key "metrics"
37 import abc
38 import math
39 import string
40 import json
41 import traceback
43 import six
45 import cmk.utils
46 import cmk.utils.render
47 import cmk.utils.plugin_registry
48 from cmk.utils.regex import regex
50 import cmk.gui.utils as utils
51 import cmk.gui.config as config
52 import cmk.gui.sites as sites
53 import cmk.gui.i18n
54 import cmk.gui.pages
55 from cmk.gui.i18n import _
56 from cmk.gui.globals import html
58 from cmk.gui.log import logger
59 from cmk.gui.exceptions import MKGeneralException, MKUserError, MKInternalError
61 # Needed for legacy (pre 1.6) plugins
62 from cmk.gui.plugins.metrics.utils import ( # pylint: disable=unused-import
63 unit_info, metric_info, check_metrics, perfometer_info, graph_info, scalar_colors, KB, MB, GB,
64 TB, PB, m, K, M, G, T, P, evaluate, get_graph_range, replace_expressions,
65 generic_graph_template, scale_symbols, hsv_to_hexrgb, render_color, parse_color,
66 parse_color_into_hexrgb, render_color_icon, darken_color, get_palette_color_by_index,
67 parse_perf_data, perfvar_translation, translate_metrics, get_graph_templates,
70 # .--Plugins-------------------------------------------------------------.
71 # | ____ _ _ |
72 # | | _ \| |_ _ __ _(_)_ __ ___ |
73 # | | |_) | | | | |/ _` | | '_ \/ __| |
74 # | | __/| | |_| | (_| | | | | \__ \ |
75 # | |_| |_|\__,_|\__, |_|_| |_|___/ |
76 # | |___/ |
77 # +----------------------------------------------------------------------+
78 # | Typical code for loading Multisite plugins of this module |
79 # '----------------------------------------------------------------------'
80 # Datastructures and functions needed before plugins can be loaded
81 loaded_with_language = False
84 def load_plugins(force):
85 global loaded_with_language
86 if loaded_with_language == cmk.gui.i18n.get_current_language() and not force:
87 return
89 utils.load_web_plugins("metrics", globals())
91 fixup_graph_info()
92 fixup_unit_info()
93 fixup_perfometer_info()
94 loaded_with_language = cmk.gui.i18n.get_current_language()
97 def fixup_graph_info():
98 # create back link from each graph to its id.
99 for graph_id, graph in graph_info.items():
100 graph["id"] = graph_id
103 def fixup_unit_info():
104 # create back link from each unit to its id.
105 for unit_id, unit in unit_info.items():
106 unit["id"] = unit_id
107 unit.setdefault("description", unit["title"])
110 def fixup_perfometer_info():
111 _convert_legacy_tuple_perfometers(perfometer_info)
112 _precalculate_some_perfometer_caches(perfometer_info)
115 # During implementation of the metric system the perfometers were first defined using
116 # tuples. This has been replaced with a dict based syntax. This function converts the
117 # old known formats from tuple to dict.
118 # All shipped perfometers have been converted to the dict format with 1.5.0i3.
119 # TODO: Remove this one day.
120 def _convert_legacy_tuple_perfometers(perfometers):
121 for index, perfometer in reversed(list(enumerate(perfometers))):
122 if isinstance(perfometer, dict):
123 continue
125 if not isinstance(perfometer, tuple) or len(perfometer) != 2:
126 raise MKGeneralException(_("Invalid perfometer declaration: %r") % perfometer)
128 # Convert legacy tuple based perfometer
129 perfometer_type, perfometer_args = perfometer[0], perfometer[1]
130 if perfometer_type in ("dual", "stacked"):
132 sub_performeters = perfometer_args[:]
133 _convert_legacy_tuple_perfometers(sub_performeters)
135 perfometers[index] = {
136 "type": perfometer_type,
137 "perfometers": sub_performeters,
140 elif perfometer_type == "linear" and len(perfometer_args) == 3:
141 required, total, label = perfometer_args
143 perfometers[index] = {
144 "type": perfometer_type,
145 "segments": required,
146 "total": total,
147 "label": label,
150 else:
151 logger.warning(
152 _("Could not convert perfometer to dict format: %r. Ignoring this one.") %
153 perfometer)
154 perfometers.pop(index)
157 def _precalculate_some_perfometer_caches(perfometers):
158 for perfometer in perfometers:
159 # Precalculate the list of metric expressions of the perfometers
160 required_expressions = _perfometer_expressions(perfometer)
162 # And also precalculate the trivial metric names that can later be used to filter
163 # perfometers without the need to evaluate the expressions.
164 required_trivial_metric_names = _required_trivial_metric_names(required_expressions)
166 perfometer["_required"] = required_expressions
167 perfometer["_required_names"] = required_trivial_metric_names
170 def _perfometer_expressions(perfometer):
171 """Returns all metric expressions of a perfometer
172 This is used for checking which perfometer can be displayed for a given service later.
174 required = []
176 if perfometer["type"] == "linear":
177 required += perfometer["segments"][:]
179 elif perfometer["type"] == "logarithmic":
180 required.append(perfometer["metric"])
182 elif perfometer["type"] in ("stacked", "dual"):
183 if "perfometers" not in perfometer:
184 raise MKGeneralException(
185 _("Perfometers of type 'stacked' and 'dual' need "
186 "the element 'perfometers' (%r)") % perfometer)
188 for sub_perfometer in perfometer["perfometers"]:
189 required += _perfometer_expressions(sub_perfometer)
191 else:
192 raise NotImplementedError(_("Invalid perfometer type: %s") % perfometer["type"])
194 if "label" in perfometer and perfometer["label"] is not None:
195 required.append(perfometer["label"][0])
196 if "total" in perfometer:
197 required.append(perfometer["total"])
199 return required
202 def _required_trivial_metric_names(required_expressions):
203 """Extract the trivial metric names from a list of expressions.
204 Ignores numeric parts. Returns None in case there is a non trivial
205 metric found. This means the trivial filtering can not be used.
207 required_metric_names = set()
209 allowed_chars = string.ascii_letters + string.digits + "_"
211 for entry in required_expressions:
212 if isinstance(entry, six.string_types):
213 if any(char not in allowed_chars for char in entry):
214 # Found a non trivial metric expression. Totally skip this mechanism
215 return None
217 required_metric_names.add(entry)
219 return required_metric_names
223 # .--Helpers-------------------------------------------------------------.
224 # | _ _ _ |
225 # | | | | | ___| |_ __ ___ _ __ ___ |
226 # | | |_| |/ _ \ | '_ \ / _ \ '__/ __| |
227 # | | _ | __/ | |_) | __/ | \__ \ |
228 # | |_| |_|\___|_| .__/ \___|_| |___/ |
229 # | |_| |
230 # +----------------------------------------------------------------------+
231 # | Various helper functions |
232 # '----------------------------------------------------------------------'
233 # A few helper function to be used by the definitions
236 def metric_to_text(metric, value=None):
237 if value is None:
238 value = metric["value"]
239 return metric["unit"]["render"](value)
242 # aliases to be compatible to old plugins
243 physical_precision = cmk.utils.render.physical_precision
244 age_human_readable = cmk.utils.render.approx_age
247 # .--Evaluation----------------------------------------------------------.
248 # | _____ _ _ _ |
249 # | | ____|_ ____ _| |_ _ __ _| |_(_) ___ _ __ |
250 # | | _| \ \ / / _` | | | | |/ _` | __| |/ _ \| '_ \ |
251 # | | |___ \ V / (_| | | |_| | (_| | |_| | (_) | | | | |
252 # | |_____| \_/ \__,_|_|\__,_|\__,_|\__|_|\___/|_| |_| |
253 # | |
254 # +----------------------------------------------------------------------+
255 # | Parsing of performance data into metrics, evaluation of expressions |
256 # '----------------------------------------------------------------------'
259 def translate_perf_data(perf_data_string, check_command=None):
260 perf_data, check_command = parse_perf_data(perf_data_string, check_command)
261 return translate_metrics(perf_data, check_command)
265 # .--Perf-O-Meters-------------------------------------------------------.
266 # | ____ __ ___ __ __ _ |
267 # | | _ \ ___ _ __ / _| / _ \ | \/ | ___| |_ ___ _ __ ___ |
268 # | | |_) / _ \ '__| |_ _____| | | |_____| |\/| |/ _ \ __/ _ \ '__/ __| |
269 # | | __/ __/ | | _|_____| |_| |_____| | | | __/ || __/ | \__ \ |
270 # | |_| \___|_| |_| \___/ |_| |_|\___|\__\___|_| |___/ |
271 # | |
272 # +----------------------------------------------------------------------+
273 # | Implementation of Perf-O-Meters |
274 # '----------------------------------------------------------------------'
277 class Perfometers(object):
278 def get_matching_perfometers(self, translated_metrics):
279 perfometers = []
280 for perfometer in perfometer_info:
281 if self._perfometer_possible(perfometer, translated_metrics):
282 perfometers.append(perfometer)
283 return perfometers
285 def _perfometer_possible(self, perfometer, translated_metrics):
286 if self._skip_perfometer_by_trivial_metrics(perfometer["_required_names"],
287 translated_metrics):
288 return False
290 for req in perfometer["_required"]:
291 try:
292 evaluate(req, translated_metrics)
293 except:
294 return False
296 if "condition" in perfometer:
297 try:
298 value, _color, _unit = evaluate(perfometer["condition"], translated_metrics)
299 if value == 0.0:
300 return False
301 except:
302 return False
304 return True
306 def _skip_perfometer_by_trivial_metrics(self, required_metric_names, translated_metrics):
307 """Whether or not a perfometer can be skipped by simple metric name matching instead of expression evaluation
309 Performance optimization: Try to reduce the amount of perfometers to evaluate by
310 comparing the strings in the "required" metrics with the translated metrics.
311 We only look at the simple "requried expressions" that don't make use of formulas.
312 In case there is a formula, we can not skip the perfometer and have to evaluate
315 if required_metric_names is None:
316 return False
318 available_metric_names = set(translated_metrics.keys())
319 return not required_metric_names.issubset(available_metric_names)
322 class MetricometerRenderer(object):
323 __metaclass__ = abc.ABCMeta
324 """Abstract base class for all metricometer renderers"""
326 @classmethod
327 def type_name(cls):
328 raise NotImplementedError()
330 def __init__(self, perfometer, translated_metrics):
331 super(MetricometerRenderer, self).__init__()
332 self._perfometer = perfometer
333 self._translated_metrics = translated_metrics
335 @abc.abstractmethod
336 def get_stack(self):
337 """Return a list of perfometer elements
339 Each element is represented by a 2 element tuple where the first element is
340 the width in px and the second element the hex color code of this element.
342 raise NotImplementedError()
344 def get_label(self):
345 """Returns the label to be shown on top of the rendered stack
347 When the perfometer type definition has a "label" element, this will be used.
348 Otherwise the perfometer type specific label of _get_type_label() will be used.
351 # "label" option in all Perf-O-Meters overrides automatic label
352 if "label" in self._perfometer:
353 if self._perfometer["label"] is None:
354 return ""
356 expr, unit_name = self._perfometer["label"]
357 value, unit, _color = evaluate(expr, self._translated_metrics)
358 if unit_name:
359 unit = unit_info[unit_name]
360 return unit["render"](value)
362 return self._get_type_label()
364 @abc.abstractmethod
365 def _get_type_label(self):
366 """Returns the label for this perfometer type"""
367 raise NotImplementedError()
369 @abc.abstractmethod
370 def get_sort_number(self):
371 """Returns the number to sort this perfometer with compared to the other
372 performeters in the current performeter sort group"""
373 raise NotImplementedError()
376 class MetricometerRendererRegistry(cmk.utils.plugin_registry.ClassRegistry):
377 def plugin_base_class(self):
378 return MetricometerRenderer
380 def plugin_name(self, plugin_class):
381 return plugin_class.type_name()
383 def get_renderer(self, perfometer, translated_metrics):
384 subclass = self[perfometer["type"]]
385 return subclass(perfometer, translated_metrics)
388 renderer_registry = MetricometerRendererRegistry()
391 @renderer_registry.register
392 class MetricometerRendererLogarithmic(MetricometerRenderer):
393 @classmethod
394 def type_name(cls):
395 return "logarithmic"
397 def __init__(self, perfometer, translated_metrics):
398 super(MetricometerRendererLogarithmic, self).__init__(perfometer, translated_metrics)
400 if self._perfometer is not None and "metric" not in self._perfometer:
401 raise MKGeneralException(
402 _("Missing key \"metric\" in logarithmic perfometer: %r") % self._perfometer)
404 def get_stack(self):
405 value, _unit, color = evaluate(self._perfometer["metric"], self._translated_metrics)
406 return [
407 self.get_stack_from_values(value, self._perfometer["half_value"],
408 self._perfometer["exponent"], color)
411 def _get_type_label(self):
412 value, unit, _color = evaluate(self._perfometer["metric"], self._translated_metrics)
413 return unit["render"](value)
415 def get_sort_number(self):
416 """Returns the number to sort this perfometer with compared to the other
417 performeters in the current performeter sort group"""
418 value, _unit, _color = evaluate(self._perfometer["metric"], self._translated_metrics)
419 return value
421 def get_stack_from_values(self, value, half_value, base, color):
422 # Negative values are printed like positive ones (e.g. time offset)
423 value = abs(float(value))
424 if value == 0.0:
425 pos = 0
426 else:
427 half_value = float(half_value)
428 h = math.log(half_value, base) # value to be displayed at 50%
429 pos = 50 + 10.0 * (math.log(value, base) - h)
430 if pos < 2:
431 pos = 2
432 if pos > 98:
433 pos = 98
435 return [(pos, color), (100 - pos, "#ffffff")]
438 @renderer_registry.register
439 class MetricometerRendererLinear(MetricometerRenderer):
440 @classmethod
441 def type_name(cls):
442 return "linear"
444 def get_stack(self):
445 entry = []
447 summed = self._get_summed_values()
449 if "total" in self._perfometer:
450 total, _unit, _color = evaluate(self._perfometer["total"], self._translated_metrics)
451 else:
452 total = summed
454 if total == 0:
455 entry.append((100.0, "#ffffff"))
457 else:
458 for ex in self._perfometer["segments"]:
459 value, _unit, color = evaluate(ex, self._translated_metrics)
460 entry.append((100.0 * value / total, color))
462 # Paint rest only, if it is positive and larger than one promille
463 if total - summed > 0.001:
464 entry.append((100.0 * (total - summed) / total, "#ffffff"))
466 return [entry]
468 def _get_type_label(self):
469 # Use unit of first metrics for output of sum. We assume that all
470 # stackes metrics have the same unit anyway
471 _value, unit, _color = evaluate(self._perfometer["segments"][0], self._translated_metrics)
472 return unit["render"](self._get_summed_values())
474 def get_sort_number(self):
475 """Use the first segment value for sorting"""
476 value, _unit, _color = evaluate(self._perfometer["segments"][0], self._translated_metrics)
477 return value
479 def _get_summed_values(self):
480 summed = 0.0
481 for ex in self._perfometer["segments"]:
482 value, _unit, _color = evaluate(ex, self._translated_metrics)
483 summed += value
484 return summed
487 @renderer_registry.register
488 class MetricometerRendererStacked(MetricometerRenderer):
489 @classmethod
490 def type_name(cls):
491 return "stacked"
493 def get_stack(self):
494 stack = []
495 for sub_perfometer in self._perfometer["perfometers"]:
496 renderer = renderer_registry.get_renderer(sub_perfometer, self._translated_metrics)
498 sub_stack = renderer.get_stack()
499 stack.append(sub_stack[0])
501 return stack
503 def _get_type_label(self):
504 sub_labels = []
505 for sub_perfometer in self._perfometer["perfometers"]:
506 renderer = renderer_registry.get_renderer(sub_perfometer, self._translated_metrics)
508 sub_label = renderer.get_label()
509 if sub_label:
510 sub_labels.append(sub_label)
512 if not sub_labels:
513 return ""
515 return " / ".join(sub_labels)
517 def get_sort_number(self):
518 """Use the number of the first stack element."""
519 sub_perfometer = self._perfometer["perfometers"][0]
520 renderer = renderer_registry.get_renderer(sub_perfometer, self._translated_metrics)
521 return renderer.get_sort_number()
524 @renderer_registry.register
525 class MetricometerRendererDual(MetricometerRenderer):
526 @classmethod
527 def type_name(cls):
528 return "dual"
530 def __init__(self, perfometer, translated_metrics):
531 super(MetricometerRendererDual, self).__init__(perfometer, translated_metrics)
533 if len(perfometer["perfometers"]) != 2:
534 raise MKInternalError(
535 _("Perf-O-Meter of type 'dual' must contain exactly "
536 "two definitions, not %d") % len(perfometer["perfometers"]))
538 def get_stack(self):
539 content = []
540 for nr, sub_perfometer in enumerate(self._perfometer["perfometers"]):
541 renderer = renderer_registry.get_renderer(sub_perfometer, self._translated_metrics)
543 sub_stack = renderer.get_stack()
544 if len(sub_stack) != 1:
545 raise MKInternalError(
546 _("Perf-O-Meter of type 'dual' must only contain plain Perf-O-Meters"))
548 half_stack = [(value / 2, color) for (value, color) in sub_stack[0]]
549 if nr == 0:
550 half_stack.reverse()
551 content += half_stack
553 return [content]
555 def _get_type_label(self):
556 sub_labels = []
557 for sub_perfometer in self._perfometer["perfometers"]:
558 renderer = renderer_registry.get_renderer(sub_perfometer, self._translated_metrics)
560 sub_label = renderer.get_label()
561 if sub_label:
562 sub_labels.append(sub_label)
564 if not sub_labels:
565 return ""
567 return " / ".join(sub_labels)
569 def get_sort_number(self):
570 """Sort by max(left, right)
572 E.g. for traffic graphs it seems to be useful to
573 make it sort by the maximum traffic independent of the direction.
575 sub_sort_numbers = []
576 for sub_perfometer in self._perfometer["perfometers"]:
577 renderer = renderer_registry.get_renderer(sub_perfometer, self._translated_metrics)
578 sub_sort_numbers.append(renderer.get_sort_number())
580 return max(*sub_sort_numbers)
584 # .--Graphs--------------------------------------------------------------.
585 # | ____ _ |
586 # | / ___|_ __ __ _ _ __ | |__ ___ |
587 # | | | _| '__/ _` | '_ \| '_ \/ __| |
588 # | | |_| | | | (_| | |_) | | | \__ \ |
589 # | \____|_| \__,_| .__/|_| |_|___/ |
590 # | |_| |
591 # +----------------------------------------------------------------------+
592 # | Implementation of time graphs - basic code, not the rendering |
593 # | Rendering of the graphs is done by PNP4Nagios, we just create PHP |
594 # | templates for PNP here. |
595 # '----------------------------------------------------------------------'
598 # .--PNP Templates-------------------------------------------------------.
599 # | ____ _ _ ____ _____ _ _ |
600 # | | _ \| \ | | _ \ |_ _|__ _ __ ___ _ __ | | __ _| |_ ___ ___ |
601 # | | |_) | \| | |_) | | |/ _ \ '_ ` _ \| '_ \| |/ _` | __/ _ \/ __| |
602 # | | __/| |\ | __/ | | __/ | | | | | |_) | | (_| | || __/\__ \ |
603 # | |_| |_| \_|_| |_|\___|_| |_| |_| .__/|_|\__,_|\__\___||___/ |
604 # | |_| |
605 # +----------------------------------------------------------------------+
606 # | Core for creating templates for PNP4Nagios from CMK graph defi- |
607 # | nitions. |
608 # '----------------------------------------------------------------------'
611 # Called with exactly one variable: the template ID. Example:
612 # "check_mk-kernel.util:guest,steal,system,user,wait".
613 @cmk.gui.pages.register("noauth:pnp_template")
614 def page_pnp_template():
615 try:
616 template_id = html.request.var("id")
618 check_command, perf_var_string = template_id.split(":", 1)
619 perf_var_names = perf_var_string.split(",")
621 # Fake performance values in order to be able to find possible graphs
622 perf_data = [(varname, 1, "", 1, 1, 1, 1) for varname in perf_var_names]
623 translated_metrics = translate_metrics(perf_data, check_command)
624 if not translated_metrics:
625 return # check not supported
627 # Collect output in string. In case of an exception to not output
628 # any definitions
629 output = ""
630 for graph_template in get_graph_templates(translated_metrics):
631 graph_code = render_graph_pnp(graph_template, translated_metrics)
632 output += graph_code
634 html.write(output)
636 except Exception:
637 html.write("An error occured:\n%s\n" % traceback.format_exc())
640 # TODO: some_value.max not yet working
641 def render_graph_pnp(graph_template, translated_metrics):
642 graph_title = None
643 vertical_label = None
645 rrdgraph_commands = ""
647 legend_precision = graph_template.get("legend_precision", 2)
648 legend_scale = graph_template.get("legend_scale", 1)
649 legend_scale_symbol = scale_symbols[legend_scale]
651 # Define one RRD variable for each of the available metrics.
652 # Note: We need to use the original name, not the translated one.
653 for var_name, metrics in translated_metrics.items():
654 rrd = "$RRDBASE$_" + metrics["orig_name"] + ".rrd"
655 scale = metrics["scale"]
656 unit = metrics["unit"]
658 if scale != 1.0:
659 rrdgraph_commands += "DEF:%s_UNSCALED=%s:1:MAX " % (var_name, rrd)
660 rrdgraph_commands += "CDEF:%s=%s_UNSCALED,%f,* " % (var_name, var_name, scale)
662 else:
663 rrdgraph_commands += "DEF:%s=%s:1:MAX " % (var_name, rrd)
665 # Scaling for legend
666 rrdgraph_commands += "CDEF:%s_LEGSCALED=%s,%f,/ " % (var_name, var_name, legend_scale)
668 # Prepare negative variants for upside-down graph
669 rrdgraph_commands += "CDEF:%s_NEG=%s,-1,* " % (var_name, var_name)
670 rrdgraph_commands += "CDEF:%s_LEGSCALED_NEG=%s_LEGSCALED,-1,* " % (var_name, var_name)
672 # Now add areas and lines to the graph
673 graph_metrics = []
675 # Graph with upside down metrics? (e.g. for Disk IO)
676 have_upside_down = False
678 # Compute width of the right column of the legend
679 max_title_length = 0
680 for nr, metric_definition in enumerate(graph_template["metrics"]):
681 if len(metric_definition) >= 3:
682 title = metric_definition[2]
683 elif not "," in metric_definition:
684 metric_name = metric_definition[0].split("#")[0]
685 mi = translated_metrics[metric_name]
686 title = mi["title"]
687 else:
688 title = ""
689 max_title_length = max(max_title_length, len(title))
691 for nr, metric_definition in enumerate(graph_template["metrics"]):
692 metric_name = metric_definition[0]
693 line_type = metric_definition[1] # "line", "area", "stack"
695 # Optional title, especially for derived values
696 if len(metric_definition) >= 3:
697 title = metric_definition[2]
698 else:
699 title = ""
701 # Prefixed minus renders the metrics in negative direction
702 if line_type[0] == '-':
703 have_upside_down = True
704 upside_down = True
705 upside_down_factor = -1
706 line_type = line_type[1:]
707 upside_down_suffix = "_NEG"
708 else:
709 upside_down = False
710 upside_down_factor = 1
711 upside_down_suffix = ""
713 if line_type == "line":
714 draw_type = "LINE"
715 draw_stack = ""
716 elif line_type == "area":
717 draw_type = "AREA"
718 draw_stack = ""
719 elif line_type == "stack":
720 draw_type = "AREA"
721 draw_stack = ":STACK"
723 # User can specify alternative color using a suffixed #aabbcc
724 if '#' in metric_name:
725 metric_name, custom_color = metric_name.split("#", 1)
726 else:
727 custom_color = None
729 commands = ""
730 # Derived value with RBN syntax (evaluated by RRDTool!).
731 if "," in metric_name:
732 # We evaluate just in order to get color and unit.
733 # TODO: beware of division by zero. All metrics are set to 1 here.
734 _value, unit, color = evaluate(metric_name, translated_metrics)
736 if "@" in metric_name:
737 expression, _explicit_unit_name = metric_name.rsplit("@", 1) # isolate expression
738 else:
739 expression = metric_name
741 # Choose a unique name for the derived variable and compute it
742 commands += "CDEF:DERIVED%d=%s " % (nr, expression)
743 if upside_down:
744 commands += "CDEF:DERIVED%d_NEG=DERIVED%d,-1,* " % (nr, nr)
746 metric_name = "DERIVED%d" % nr
747 # Scaling and upsidedown handling for legend
748 commands += "CDEF:%s_LEGSCALED=%s,%f,/ " % (metric_name, metric_name, legend_scale)
749 if upside_down:
750 commands += "CDEF:%s_LEGSCALED%s=%s,%f,/ " % (
751 metric_name, upside_down_suffix, metric_name, legend_scale * upside_down_factor)
753 else:
754 mi = translated_metrics[metric_name]
755 if not title:
756 title = mi["title"]
757 color = parse_color_into_hexrgb(mi["color"])
758 unit = mi["unit"]
760 if custom_color:
761 color = "#" + custom_color
763 # Paint the graph itself
764 # TODO: Die Breite des Titels intelligent berechnen. Bei legend = "mirrored" muss man die
765 # Vefügbare Breite ermitteln und aufteilen auf alle Titel
766 right_pad = " " * (max_title_length - len(title))
767 commands += "%s:%s%s%s:\"%s%s\"%s " % (draw_type, metric_name, upside_down_suffix, color,
768 title.replace(":", "\\:"), right_pad, draw_stack)
769 if line_type == "area":
770 commands += "LINE:%s%s%s " % (metric_name, upside_down_suffix,
771 render_color(darken_color(parse_color(color), 0.2)))
773 unit_symbol = unit["symbol"]
774 if unit_symbol == "%":
775 unit_symbol = "%%"
776 else:
777 unit_symbol = " " + unit_symbol
779 graph_metrics.append((metric_name, unit_symbol, commands))
781 # Use title and label of this metrics as default for the graph
782 if title and not graph_title:
783 graph_title = title
784 if not vertical_label:
785 vertical_label = unit["title"]
787 # Now create the rrdgraph commands for all metrics - according to the choosen layout
788 for metric_name, unit_symbol, commands in graph_metrics:
789 rrdgraph_commands += commands
790 legend_symbol = unit_symbol
791 if unit_symbol and unit_symbol[0] == " ":
792 legend_symbol = " %s%s" % (legend_scale_symbol, unit_symbol[1:])
793 for what, what_title in [("AVERAGE", _("average")), ("MAX", _("max")), ("LAST", _("last"))]:
794 rrdgraph_commands += "GPRINT:%%s_LEGSCALED:%%s:\"%%%%8.%dlf%%s %%s\" " % legend_precision % \
795 (metric_name, what, legend_symbol, what_title)
796 rrdgraph_commands += "COMMENT:\"\\n\" "
798 # For graphs with both up and down, paint a gray rule at 0
799 if have_upside_down:
800 rrdgraph_commands += "HRULE:0#c0c0c0 "
802 # Now compute the arguments for the command line of rrdgraph
803 rrdgraph_arguments = ""
805 graph_title = graph_template.get("title", graph_title)
806 vertical_label = graph_template.get("vertical_label", vertical_label)
808 rrdgraph_arguments += " --vertical-label %s --title %s " % (cmk.utils.quote_shell_string(
809 vertical_label or " "), cmk.utils.quote_shell_string(graph_title))
811 min_value, max_value = get_graph_range(graph_template, translated_metrics)
812 if min_value is not None and max_value is not None:
813 rrdgraph_arguments += " -l %f -u %f" % (min_value, max_value)
814 else:
815 rrdgraph_arguments += " -l 0"
817 return graph_title + "\n" + rrdgraph_arguments + "\n" + rrdgraph_commands + "\n"
821 # .--Hover-Graph---------------------------------------------------------.
822 # | _ _ ____ _ |
823 # | | | | | _____ _____ _ __ / ___|_ __ __ _ _ __ | |__ |
824 # | | |_| |/ _ \ \ / / _ \ '__|____| | _| '__/ _` | '_ \| '_ \ |
825 # | | _ | (_) \ V / __/ | |_____| |_| | | | (_| | |_) | | | | |
826 # | |_| |_|\___/ \_/ \___|_| \____|_| \__,_| .__/|_| |_| |
827 # | |_| |
828 # '----------------------------------------------------------------------'
831 def cmk_graphs_possible(site_id=None):
832 try:
833 return not config.force_pnp_graphing \
834 and browser_supports_canvas() \
835 and site_is_running_cmc(site_id)
836 except:
837 return False
840 # If site_id is None then we return True if at least
841 # one site is running CMC
842 def site_is_running_cmc(site_id):
843 if site_id:
844 return sites.state(site_id, {}).get("program_version", "").startswith("Check_MK")
846 for status in sites.states().values():
847 if status.get("program_version", "").startswith("Check_MK"):
848 return True
849 return False
852 def browser_supports_canvas():
853 user_agent = html.request.user_agent
855 if 'MSIE' in user_agent:
856 matches = regex(r'MSIE ([0-9]{1,}[\.0-9]{0,})').search(user_agent)
857 if matches:
858 ie_version = float(matches.group(1))
859 if ie_version >= 9.0:
860 return True
862 # Trying to deal with the IE compatiblity mode to detect the real IE version
863 matches = regex(r'Trident/([0-9]{1,}[\.0-9]{0,})').search(user_agent)
864 if matches:
865 trident_version = float(matches.group(1)) + 4
866 if trident_version >= 9.0:
867 return True
869 return False
870 else:
871 return True
874 def get_graph_template_by_source(graph_templates, source):
875 graph_template = None
876 for source_nr, template in enumerate(graph_templates):
877 if source == source_nr + 1:
878 graph_template = template
879 break
880 return graph_template
883 # This page is called for the popup of the graph icon of hosts/services.
884 @cmk.gui.pages.register("host_service_graph_popup")
885 def page_host_service_graph_popup():
886 site_id = html.request.var('site')
887 host_name = html.request.var('host_name')
888 service_description = html.get_unicode_input('service')
890 # TODO: Refactor this to some OO based approach
891 if cmk_graphs_possible(site_id):
892 import cmk.gui.cee.plugins.metrics.graphs as graphs
893 graphs.host_service_graph_popup_cmk(site_id, host_name, service_description)
894 else:
895 host_service_graph_popup_pnp(site_id, host_name, service_description)
898 def host_service_graph_popup_pnp(site, host_name, service_description):
899 pnp_host = cmk.utils.pnp_cleanup(host_name)
900 pnp_svc = cmk.utils.pnp_cleanup(service_description)
901 url_prefix = config.site(site)["url_prefix"]
903 if html.mobile:
904 url = url_prefix + ("pnp4nagios/index.php?kohana_uri=/mobile/popup/%s/%s" % \
905 (html.urlencode(pnp_host), html.urlencode(pnp_svc)))
906 else:
907 url = url_prefix + ("pnp4nagios/index.php/popup?host=%s&srv=%s" % \
908 (html.urlencode(pnp_host), html.urlencode(pnp_svc)))
910 html.write(url)
914 # .--Graph Dashlet-------------------------------------------------------.
915 # | ____ _ ____ _ _ _ |
916 # | / ___|_ __ __ _ _ __ | |__ | _ \ __ _ ___| |__ | | ___| |_ |
917 # | | | _| '__/ _` | '_ \| '_ \ | | | |/ _` / __| '_ \| |/ _ \ __| |
918 # | | |_| | | | (_| | |_) | | | | | |_| | (_| \__ \ | | | | __/ |_ |
919 # | \____|_| \__,_| .__/|_| |_| |____/ \__,_|___/_| |_|_|\___|\__| |
920 # | |_| |
921 # +----------------------------------------------------------------------+
922 # | This page handler is called by graphs embedded in a dashboard. |
923 # '----------------------------------------------------------------------'
926 @cmk.gui.pages.register("graph_dashlet")
927 def page_graph_dashlet():
928 spec = html.request.var("spec")
929 if not spec:
930 raise MKUserError("spec", _("Missing spec parameter"))
931 graph_identification = json.loads(html.request.var("spec"))
933 render = html.request.var("render")
934 if not render:
935 raise MKUserError("render", _("Missing render parameter"))
936 custom_graph_render_options = json.loads(html.request.var("render"))
938 # TODO: Refactor this to some OO based approach
939 if cmk_graphs_possible():
940 import cmk.gui.cee.plugins.metrics.graphs as graphs
941 graphs.host_service_graph_dashlet_cmk(graph_identification, custom_graph_render_options)
942 elif graph_identification[0] == "template":
943 host_service_graph_dashlet_pnp(graph_identification)
944 else:
945 html.write(_("This graph can not be rendered."))
948 def host_service_graph_dashlet_pnp(graph_identification):
949 site = graph_identification[1]["site"]
950 source = int(graph_identification[1]["graph_index"])
952 pnp_host = cmk.utils.pnp_cleanup(graph_identification[1]["host_name"])
953 pnp_svc = cmk.utils.pnp_cleanup(graph_identification[1]["service_description"])
954 url_prefix = config.site(site)["url_prefix"]
956 pnp_theme = html.get_theme()
957 if pnp_theme == "classic":
958 pnp_theme = "multisite"
960 html.write(url_prefix + "pnp4nagios/index.php/image?host=%s&srv=%s&source=%d&view=%s&theme=%s" % \
961 (html.urlencode(pnp_host), html.urlencode(pnp_svc), source, html.request.var("timerange"), pnp_theme))
965 # .--Metrics Table-------------------------------------------------------.
966 # | __ __ _ _ _____ _ _ |
967 # | | \/ | ___| |_ _ __(_) ___ ___ |_ _|_ _| |__ | | ___ |
968 # | | |\/| |/ _ \ __| '__| |/ __/ __| | |/ _` | '_ \| |/ _ \ |
969 # | | | | | __/ |_| | | | (__\__ \ | | (_| | |_) | | __/ |
970 # | |_| |_|\___|\__|_| |_|\___|___/ |_|\__,_|_.__/|_|\___| |
971 # | |
972 # +----------------------------------------------------------------------+
973 # | Renders a simple table with all metrics of a host or service |
974 # '----------------------------------------------------------------------'
977 def render_metrics_table(translated_metrics, host_name, service_description):
978 output = "<table class=metricstable>"
979 for metric_name, metric in sorted(
980 translated_metrics.items(),
981 cmp=lambda a, b: cmp(a[1]["title"], b[1]["title"]),
983 output += "<tr>"
984 output += "<td class=color>%s</td>" % render_color_icon(metric["color"])
985 output += "<td>%s:</td>" % metric["title"]
986 output += "<td class=value>%s</td>" % metric["unit"]["render"](metric["value"])
987 if cmk_graphs_possible():
988 output += "<td>"
989 output += html.render_popup_trigger(
990 html.render_icon(
991 "custom_graph",
992 title=_("Add this metric to a custom graph"),
993 cssclass="iconbutton"),
994 ident="add_metric_to_graph_" + host_name + ";" + service_description,
995 what="add_metric_to_custom_graph",
996 url_vars=[
997 ("host", host_name),
998 ("service", service_description),
999 ("metric", metric_name),
1001 output += "</td>"
1002 output += "</tr>"
1003 output += "</table>"
1004 return output