Add specific visualization for labels depending on their source
[check_mk.git] / cmk / gui / plugins / views / utils.py
blob4c8304c8e94b66a9c4b3f7a80db572e28c26c9f8
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.
26 """Module to hold shared code for internals and the plugins"""
28 # TODO: More feature related splitting up would be better
30 import abc
31 import os
32 import time
33 import re
34 import hashlib
35 import traceback
36 import six
38 import livestatus
40 import cmk.utils.plugin_registry
41 import cmk.utils.render
42 import cmk.utils.regex
44 import cmk.gui.config as config
45 import cmk.gui.sites as sites
46 import cmk.gui.visuals as visuals
47 import cmk.gui.forms as forms
48 import cmk.gui.utils
49 import cmk.gui.view_utils
50 from cmk.gui.valuespec import ValueSpec # pylint: disable=unused-import
51 from cmk.gui.log import logger
52 from cmk.gui.htmllib import HTML
53 from cmk.gui.i18n import _
54 from cmk.gui.globals import html, current_app
55 from cmk.gui.exceptions import MKGeneralException
56 from cmk.gui.display_options import display_options
57 from cmk.gui.permissions import permission_registry
58 from cmk.gui.view_utils import render_tag_groups, render_labels # pylint: disable=unused-import
61 # TODO: Better name it PainterOptions or DisplayOptions? There are options which only affect
62 # painters, but some which affect generic behaviour of the views, so DisplayOptions might
63 # be better.
64 class PainterOptions(object):
65 """Painter options are settings that can be changed per user per view.
66 These options are controlled throught the painter options form which
67 is accessible through the small monitor icon on the top left of the
68 views."""
70 # TODO: We should have some View instance that uses an object of this class as helper instead,
71 # but this would be a bigger change involving a lot of view rendering code.
72 @classmethod
73 def get_instance(cls):
74 """Use the request globals to prevent multiple instances during a request"""
75 if "painter_options" not in current_app.g:
76 current_app.g["painter_options"] = cls()
77 return current_app.g["painter_options"]
79 def __init__(self):
80 super(PainterOptions, self).__init__()
81 # The names of the painter options used by the current view
82 self._used_option_names = []
83 # The effective options for this view
84 self._options = {}
86 def load(self, view_name=None):
87 self._load_from_config(view_name)
89 # Load the options to be used for this view
90 def _load_used_options(self, view):
91 options = set([])
93 for cell in view.group_cells + view.row_cells:
94 options.update(cell.painter_options())
96 # Also layouts can register painter options
97 layout_class = layout_registry.get(view.spec.get("layout"))
98 if layout_class:
99 options.update(layout_class().painter_options)
101 # TODO: Improve sorting. Add a sort index?
102 self._used_option_names = sorted(options)
104 def _load_from_config(self, view_name):
105 if self._is_anonymous_view(view_name):
106 return # never has options
108 if not self.painter_options_permitted():
109 return
111 # Options are stored per view. Get all options for all views
112 vo = config.user.load_file("viewoptions", {})
113 self._options = vo.get(view_name, {})
115 def _is_anonymous_view(self, view_name):
116 return view_name is None
118 def save_to_config(self, view_name):
119 vo = config.user.load_file("viewoptions", {}, lock=True)
120 vo[view_name] = self._options
121 config.user.save_file("viewoptions", vo)
123 def update_from_url(self, view):
124 self._load_used_options(view)
126 if not self.painter_option_form_enabled():
127 return
129 if html.request.has_var("_reset_painter_options"):
130 self._clear_painter_options(view.name)
131 return
133 elif html.request.has_var("_update_painter_options"):
134 self._set_from_submitted_form(view.name)
136 def _set_from_submitted_form(self, view_name):
137 # TODO: Remove all keys that are in painter_option_registry
138 # but not in self._used_option_names
140 modified = False
141 for option_name in self._used_option_names:
142 # Get new value for the option from the value spec
143 vs = self.get_valuespec_of(option_name)
144 value = vs.from_html_vars("po_%s" % option_name)
146 if not self._is_set(option_name) or self.get(option_name) != value:
147 modified = True
149 self.set(option_name, value)
151 if modified:
152 self.save_to_config(view_name)
154 def _clear_painter_options(self, view_name):
155 # TODO: This never removes options that are not existant anymore
156 modified = False
157 for name in painter_option_registry.keys():
158 try:
159 del self._options[name]
160 modified = True
161 except KeyError:
162 pass
164 if modified:
165 self.save_to_config(view_name)
167 # Also remove the options from current html vars. Otherwise the
168 # painter option form will display the just removed options as
169 # defaults of the painter option form.
170 for varname, _value in list(html.request.itervars(prefix="po_")):
171 html.request.del_var(varname)
173 def get_valuespec_of(self, name):
174 return painter_option_registry[name]().valuespec
176 def _is_set(self, name):
177 return name in self._options
179 # Sets a painter option value (only for this request). Is not persisted!
180 def set(self, name, value):
181 self._options[name] = value
183 # Returns either the set value, the provided default value or if none
184 # provided, it returns the default value of the valuespec.
185 def get(self, name, dflt=None):
186 if dflt is None:
187 try:
188 dflt = self.get_valuespec_of(name).default_value()
189 except KeyError:
190 # Some view options (that are not declared as display options)
191 # like "refresh" don't have a valuespec. So they need to default
192 # to None.
193 # TODO: Find all occurences and simply declare them as "invisible"
194 # painter options.
195 pass
196 return self._options.get(name, dflt)
198 # Not falling back to a default value, simply returning None in case
199 # the option is not set.
200 def get_without_default(self, name):
201 return self._options.get(name)
203 def get_all(self):
204 return self._options
206 def painter_options_permitted(self):
207 return config.user.may("general.painter_options")
209 def painter_option_form_enabled(self):
210 return self._used_option_names and self.painter_options_permitted()
212 def show_form(self, view):
213 self._load_used_options(view)
215 if not display_options.enabled(display_options.D) or not self.painter_option_form_enabled():
216 return
218 html.open_div(id_="painteroptions", class_=["view_form"], style="display: none;")
219 html.begin_form("painteroptions")
220 forms.header(_("Display Options"))
221 for name in self._used_option_names:
222 vs = self.get_valuespec_of(name)
223 forms.section(vs.title())
224 # TODO: Possible improvement for vars which default is specified
225 # by the view: Don't just default to the valuespecs default. Better
226 # use the view default value here to get the user the current view
227 # settings reflected.
228 vs.render_input("po_%s" % name, self.get(name))
229 forms.end()
231 html.button("_update_painter_options", _("Submit"), "submit")
232 html.button("_reset_painter_options", _("Reset"), "submit")
234 html.hidden_fields()
235 html.end_form()
236 html.close_div()
239 # Calculates a uniq id for each data row which identifies the current
240 # row accross different page loadings.
241 def row_id(view, row):
242 key = u''
243 for col in data_source_registry[view['datasource']]().id_keys:
244 key += u'~%s' % row[col]
245 return hashlib.sha256(key.encode('utf-8')).hexdigest()
248 # The Group-value of a row is used for deciding whether
249 # two rows are in the same group or not
250 def group_value(row, group_cells):
251 group = []
252 for cell in group_cells:
253 painter = cell.painter()
255 group_by_val = painter.group_by(row)
256 if group_by_val is not None:
257 group.append(group_by_val)
259 else:
260 for c in painter.columns:
261 if c in row:
262 group.append(row[c])
264 return _create_dict_key(group)
267 def _create_dict_key(value):
268 if isinstance(value, (list, tuple)):
269 return tuple(map(_create_dict_key, value))
270 elif isinstance(value, dict):
271 return tuple([(k, _create_dict_key(v)) for (k, v) in sorted(value.items())])
272 return value
275 class PainterOption(object):
276 __metaclass__ = abc.ABCMeta
278 @abc.abstractproperty
279 def ident(self):
280 # type: () -> str
281 """The identity of a painter option. One word, may contain alpha numeric characters"""
282 raise NotImplementedError()
284 @abc.abstractproperty
285 def valuespec(self):
286 # type: () -> ValueSpec
287 raise NotImplementedError()
290 class ViewPainterOptionRegistry(cmk.utils.plugin_registry.ClassRegistry):
291 def plugin_base_class(self):
292 return PainterOption
294 def plugin_name(self, plugin_class):
295 return plugin_class().ident
298 painter_option_registry = ViewPainterOptionRegistry()
301 class Layout(object):
302 __metaclass__ = abc.ABCMeta
304 @abc.abstractproperty
305 def ident(self):
306 # type: () -> str
307 """The identity of a layout. One word, may contain alpha numeric characters"""
308 raise NotImplementedError()
310 @abc.abstractproperty
311 def title(self):
312 # type: () -> Text
313 """Short human readable title of the layout"""
314 raise NotImplementedError()
316 @abc.abstractmethod
317 def render(self, rows, view, group_cells, cells, num_columns, show_checkboxes):
318 # type: (List, Dict, List[Cell], List[Cell], int, bool) -> None
319 """Render the given data in this layout"""
320 raise NotImplementedError()
322 @abc.abstractproperty
323 def can_display_checkboxes(self):
324 # type: () -> bool
325 """Whether this layout can display checkboxes for selecting rows"""
326 raise NotImplementedError()
328 @abc.abstractproperty
329 def is_hidden(self):
330 # type: () -> bool
331 """Whether this should be hidden from the user (e.g. in the view editor layout choice)"""
332 raise NotImplementedError()
334 @property
335 def painter_options(self):
336 # type: () -> List[str]
337 """Returns the painter option identities used by this layout"""
338 return []
340 @property
341 def has_individual_csv_export(self):
342 # type: () -> bool
343 """Whether this layout has an individual CSV export implementation"""
344 return False
346 def csv_export(self, rows, view, group_cells, cells):
347 # type: (List, Dict, List[Cell], List[Cell]) -> None
348 """Render the given data using this layout for CSV"""
349 pass
352 class ViewLayoutRegistry(cmk.utils.plugin_registry.ClassRegistry):
353 def plugin_base_class(self):
354 return Layout
356 def plugin_name(self, plugin_class):
357 return plugin_class().ident
359 def get_choices(self):
360 choices = []
361 for plugin_class in self.values():
362 layout = plugin_class()
363 if layout.is_hidden:
364 continue
366 choices.append((layout.ident, layout.title))
368 return choices
371 layout_registry = ViewLayoutRegistry()
374 class CommandGroup(object):
375 __metaclass__ = abc.ABCMeta
377 @abc.abstractproperty
378 def ident(self):
379 # type: () -> str
380 """The identity of a command group. One word, may contain alpha numeric characters"""
381 raise NotImplementedError()
383 @abc.abstractproperty
384 def title(self):
385 # type: () -> Text
386 raise NotImplementedError()
388 @abc.abstractproperty
389 def sort_index(self):
390 # type: () -> int
391 raise NotImplementedError()
394 class CommandGroupRegistry(cmk.utils.plugin_registry.ClassRegistry):
395 def plugin_base_class(self):
396 return CommandGroup
398 def plugin_name(self, plugin_class):
399 return plugin_class().ident
402 command_group_registry = CommandGroupRegistry()
405 # TODO: Kept for pre 1.6 compatibility
406 def register_command_group(ident, title, sort_index):
407 cls = type(
408 "LegacyCommandGroup%s" % ident.title(), (CommandGroup,), {
409 "_ident": ident,
410 "_title": title,
411 "_sort_index": sort_index,
412 "ident": property(lambda s: s._ident),
413 "title": property(lambda s: s._title),
414 "sort_index": property(lambda s: s._sort_index),
416 command_group_registry.register(cls)
419 class Command(object):
420 __metaclass__ = abc.ABCMeta
422 @abc.abstractproperty
423 def ident(self):
424 # type: () -> str
425 """The identity of a command. One word, may contain alpha numeric characters"""
426 raise NotImplementedError()
428 @abc.abstractproperty
429 def title(self):
430 # type: () -> Text
431 raise NotImplementedError()
433 @abc.abstractproperty
434 def permission(self):
435 # type: () -> Type[Permission]
436 raise NotImplementedError()
438 @abc.abstractproperty
439 def tables(self):
440 # type: () -> List[str]
441 """List of livestatus table identities the action may be used with"""
442 raise NotImplementedError()
444 def render(self, what):
445 # type: (str) -> None
446 raise NotImplementedError()
448 @abc.abstractmethod
449 def action(self, cmdtag, spec, row, row_index, num_rows):
450 # type: (str, str, dict, int, int) -> Optional[Tuple[List[str], Text]]
451 raise NotImplementedError()
453 @property
454 def group(self):
455 # type: () -> Type[CommandGroup]
456 """The command group the commmand belongs to"""
457 return command_group_registry["various"]
459 @property
460 def only_view(self):
461 # type: () -> Optional[str]
462 """View name to show a view exclusive command for"""
463 return None
465 def executor(self, command, site):
466 # type: (str, str) -> Callable
467 """Function that is called to execute this action"""
468 sites.live().command("[%d] %s" % (int(time.time()), command), site)
471 class CommandRegistry(cmk.utils.plugin_registry.ClassRegistry):
472 def plugin_base_class(self):
473 return Command
475 def plugin_name(self, plugin_class):
476 return plugin_class().ident
479 command_registry = CommandRegistry()
482 # TODO: Kept for pre 1.6 compatibility
483 def register_legacy_command(spec):
484 ident = re.sub("[^a-zA-Z]", "", spec["title"]).lower()
485 cls = type(
486 "LegacyCommand%s" % ident.title(), (Command,), {
487 "_ident": ident,
488 "_spec": spec,
489 "ident": property(lambda s: s._ident),
490 "title": property(lambda s: s._spec["title"]),
491 "permission": property(lambda s: permission_registry[s._spec["permission"]]),
492 "tables": property(lambda s: s._spec["tables"]),
493 "render": lambda s: s._spec["render"](),
494 "action": lambda s, cmdtag, spec, row, row_index, num_rows: s._spec["action"]
495 (cmdtag, spec, row),
496 "group": lambda s: command_group_registry[s._spec.get("group", "various")],
497 "only_view": lambda s: s._spec.get("only_view"),
499 command_registry.register(cls)
502 class DataSource(object):
503 """Provider of rows for the views (basically tables of data) in the GUI"""
504 __metaclass__ = abc.ABCMeta
506 @abc.abstractproperty
507 def ident(self):
508 # type: () -> str
509 """The identity of a data source. One word, may contain alpha numeric characters"""
510 raise NotImplementedError()
512 @abc.abstractproperty
513 def title(self):
514 # type: () -> Text
515 """Used as display-string for the datasource in the GUI (e.g. view editor)"""
516 raise NotImplementedError()
518 @abc.abstractproperty
519 def table(self):
520 # type: () -> RowTable
521 """Returns a table objec that can provide a list of rows for the provided
522 query using the query() method."""
523 raise NotImplementedError()
525 @abc.abstractproperty
526 def infos(self):
527 # type: () -> List[str]
528 """Infos that are available with this data sources
530 A info is used to create groups out of single painters and filters.
531 e.g. 'host' groups all painters and filters which begin with "host_".
532 Out of this declaration multisite knows which filters or painters are
533 available for the single datasources."""
534 raise NotImplementedError()
536 @property
537 def merge_by(self):
538 # type: () -> Optional[str]
540 1. Results in fetching these columns from the datasource.
541 2. Rows from different sites are merged together. For example members
542 of hostgroups which exist on different sites are merged together to
543 show the user one big hostgroup.
545 return None
547 @property
548 def add_columns(self):
549 # type: () -> List[str]
550 """These columns are requested automatically in addition to the
551 other needed columns."""
552 return []
554 @property
555 def add_headers(self):
556 # type: () -> str
557 """additional livestatus headers to add to each call"""
558 return ""
560 @abc.abstractproperty
561 def keys(self):
562 # type: () -> List[str]
563 """columns which must be fetched in order to execute commands on
564 the items (= in order to identify the items and gather all information
565 needed for constructing Nagios commands)
566 those columns are always fetched from the datasource for each item"""
567 raise NotImplementedError()
569 @abc.abstractproperty
570 def id_keys(self):
571 # type: () -> List[str]
572 """These are used to generate a key which is unique for each data row
573 is used to identify an item between http requests"""
574 raise NotImplementedError()
576 @property
577 def join(self):
578 # type: () -> Optional[Tuple]
579 """A view can display e.g. host-rows and include information from e.g.
580 the service table to create a column which shows e.g. the state of one
581 service.
582 With this attibute it is configured which tables can be joined into
583 this table and by which attribute. It must be given as tuple, while
584 the first argument is the name of the table to be joined and the second
585 argument is the column in the master table (in this case hosts) which
586 is used to match the rows of the master and slave table."""
587 return None
589 @property
590 def join_key(self):
591 # type: () -> Optional[str]
592 """Each joined column in the view can have a 4th attribute which is
593 used as value for this column to filter the datasource query
594 to get the matching row of the slave table."""
595 return None
597 @property
598 def ignore_limit(self):
599 # type: () -> bool
600 """Ignore the soft/hard query limits in view.py/query_data(). This
601 fixes stats queries on e.g. the log table."""
602 return False
604 @property
605 def auth_domain(self):
606 # type: () -> str
607 """Querying a table might require to use another auth domain than
608 the default one (read). When this is set, the given auth domain
609 will be used while fetching the data for this datasource from
610 livestatus."""
611 return "read"
613 @property
614 def time_filters(self):
615 # type: () -> List[str]
616 return []
618 @property
619 def link_filters(self):
620 # type: () -> Dict[str, str]
621 """When the single info "hostgroup" is used, use the "opthostgroup" filter
622 to handle the data provided by the single_spec value of the "hostgroup"
623 info, which is in fact the name of the wanted hostgroup"""
624 return {}
626 # TODO: This can be cleaned up later
627 def post_process(self, rows):
628 # type: (List[Dict]) -> List[Dict]
629 """Optional function to postprocess the resulting data after executing
630 the regular data fetching"""
631 return rows
634 class DataSourceLivestatus(DataSource):
635 """Base class for all simple data sources which 1:1 base on a livestatus table"""
637 @property
638 def table(self):
639 return RowTableLivestatus(self.ident)
642 class DataSourceRegistry(cmk.utils.plugin_registry.ClassRegistry):
643 def plugin_base_class(self):
644 return DataSource
646 def plugin_name(self, plugin_class):
647 return plugin_class().ident
649 # TODO: Sort the datasources by (assumed) common usage
650 def data_source_choices(self):
651 datasources = []
652 for ident, ds_class in self.items():
653 datasources.append((ident, ds_class().title))
654 return sorted(datasources, key=lambda x: x[1])
657 data_source_registry = DataSourceRegistry()
660 class RowTable(object):
661 __metaclass__ = abc.ABCMeta
663 @abc.abstractmethod
664 def query(self, view, columns, query, only_sites, limit, all_active_filters):
665 raise NotImplementedError()
668 class RowTableLivestatus(RowTable):
669 def __init__(self, table_name):
670 super(RowTableLivestatus, self).__init__()
671 self._table_name = table_name
673 def query(self, view, columns, query, only_sites, limit, all_active_filters):
674 # TODO: Move query_data into this class
675 return query_data(
676 view.datasource, columns, query, only_sites, view.row_limit, tablename=self._table_name)
679 # TODO: Return value of render() could be cleaned up e.g. to a named tuple with an
680 # optional CSS class. A lot of painters don't specify CSS classes.
681 # TODO: Since we have the reporting also working with the painters it could be useful
682 # to make the render function return structured data which can then be rendered for
683 # HTML and PDF.
684 # TODO: A lot of painter classes simply display plain livestatus column values. These
685 # could be replaced with some simpler generic definition.
686 class Painter(object):
687 """A painter computes HTML code based on information from a data row and
688 creates a CSS class for one display column.
690 Please note, that there is no
691 1:1 relation between data columns and display columns. A painter can
692 make use of more than one data columns. One example is the current
693 service state. It uses the columns "service_state" and "has_been_checked".
696 __metaclass__ = abc.ABCMeta
698 @abc.abstractproperty
699 def ident(self):
700 # type: () -> str
701 """The identity of a painter. One word, may contain alpha numeric characters"""
702 raise NotImplementedError()
704 @abc.abstractproperty
705 def title(self):
706 # type: () -> Text
707 """Used as display string for the painter in the GUI (e.g. view editor)"""
708 raise NotImplementedError()
710 @abc.abstractproperty
711 def columns(self):
712 # type: () -> List[str]
713 """Livestatus columns needed for this painter"""
714 raise NotImplementedError()
716 @abc.abstractmethod
717 def render(self, row, cell):
718 # type: (dict, Cell) -> Tuple[str, str]
719 """Renders the painter for the given row
720 The paint function gets one argument: A data row, which is a python
721 dictionary representing one data object (host, service, ...). Its
722 keys are the column names, its values the actual values from livestatus
723 (typed: numbers are float or int, not string)
725 The paint function must return a pair of two strings: The HTML code
726 for painting the column and a CSS class for the TD of the column.
727 That class is optional and set to "" in most cases. Currently CSS
728 styles are not modular and all defined in check_mk.css. This will
729 change in future."""
730 raise NotImplementedError()
732 @property
733 def short_title(self):
734 # type: () -> Text
735 """Used as display string for the painter e.g. as table header
736 Falls back to the full title if no short title is given"""
737 return self.title
739 def group_by(self, row):
740 # type: (dict) -> Optional[Union[str, Tuple]]
741 """When a value is returned, this is used instead of the value produced by self.paint()"""
742 return None
744 @property
745 def parameters(self):
746 # type: () -> Optional[ValueSpec]
747 """Returns either the valuespec of the painter parameters or None"""
748 return None
750 @property
751 def painter_options(self):
752 # type: () -> List[str]
753 """Returns a list of painter option names that affect this painter"""
754 return []
756 @property
757 def printable(self):
758 # type: () -> Union[bool, str]
760 True : Is printable in PDF
761 False : Is not printable at all
762 "<string>" : ID of a painter_printer (Reporting module)
764 return True
766 @property
767 def sorter(self):
768 # type: () -> Optional[str]
769 """Returns the optional name of the sorter for this painter"""
770 return None
772 # TODO: Cleanup this hack
773 @property
774 def load_inv(self):
775 # type: () -> bool
776 """Whether or not to load the HW/SW inventory for this column"""
777 return False
780 class PainterRegistry(cmk.utils.plugin_registry.ClassRegistry):
781 def plugin_base_class(self):
782 return Painter
784 def plugin_name(self, plugin_class):
785 return plugin_class().ident
788 painter_registry = PainterRegistry()
791 # Kept for pre 1.6 compatibility. But also the inventory.py uses this to
792 # register some painters dynamically
793 def register_painter(ident, spec):
794 cls = type(
795 "LegacyPainter%s" % ident.title(), (Painter,), {
796 "_ident": ident,
797 "_spec": spec,
798 "ident": property(lambda s: s._ident),
799 "title": property(lambda s: s._spec["title"]),
800 "columns": property(lambda s: s._spec["columns"]),
801 "render": lambda self, row, cell: spec["paint"](row),
802 "short_title": property(lambda s: s._spec.get("short", s.title)),
803 "group_by": property(lambda s: s._spec.get("groupby")),
804 "parameters": property(lambda s: s._spec.get("params")),
805 "painter_options": property(lambda s: s._spec.get("options", [])),
806 "printable": property(lambda s: s._spec.get("printable", True)),
807 "sorter": property(lambda s: s._spec.get("sorter", None)),
808 "load_inv": property(lambda s: s._spec.get("load_inv", False)),
810 painter_registry.register(cls)
813 class Sorter(object):
814 """A sorter is used for allowing the user to sort the queried data
815 according to a certain logic."""
817 __metaclass__ = abc.ABCMeta
819 @abc.abstractproperty
820 def ident(self):
821 # type: () -> str
822 """The identity of a sorter. One word, may contain alpha numeric characters"""
823 raise NotImplementedError()
825 @abc.abstractproperty
826 def title(self):
827 # type: () -> Text
828 """Used as display string for the sorter in the GUI (e.g. view editor)"""
829 raise NotImplementedError()
831 @abc.abstractproperty
832 def columns(self):
833 # type: () -> List[str]
834 """Livestatus columns needed for this sorter"""
835 raise NotImplementedError()
837 @abc.abstractmethod
838 def cmp(self, r1, r2):
839 # type: (dict, dict) -> int
840 """The function cmp does the actual sorting. During sorting it
841 will be called with two data rows as arguments and must
842 return -1, 0 or 1:
844 -1: The first row is smaller than the second (should be output first)
845 0: Both rows are equivalent
846 1: The first row is greater than the second.
848 The rows are dictionaries from column names to values. Each row
849 represents one item in the Livestatus table, for example one host,
850 one service, etc."""
851 raise NotImplementedError()
853 @property
854 def _args(self):
855 # type: () -> Optional[List]
856 """Optional list of arguments for the cmp function"""
857 return None
859 # TODO: Cleanup this hack
860 @property
861 def load_inv(self):
862 # type: () -> bool
863 """Whether or not to load the HW/SW inventory for this column"""
864 return False
867 class SorterRegistry(cmk.utils.plugin_registry.ClassRegistry):
868 def plugin_base_class(self):
869 return Sorter
871 def plugin_name(self, plugin_class):
872 return plugin_class().ident
875 sorter_registry = SorterRegistry()
878 # Kept for pre 1.6 compatibility. But also the inventory.py uses this to
879 # register some painters dynamically
880 def register_sorter(ident, spec):
881 cls = type(
882 "LegacySorter%s" % ident.title(), (Sorter,), {
883 "_ident": ident,
884 "_spec": spec,
885 "ident": property(lambda s: s._ident),
886 "title": property(lambda s: s._spec["title"]),
887 "columns": property(lambda s: s._spec["columns"]),
888 "load_inv": property(lambda s: s._spec.get("load_inv", False)),
889 "cmp": spec["cmp"],
891 sorter_registry.register(cls)
894 # TODO: Refactor to plugin_registries
895 multisite_builtin_views = {}
896 view_hooks = {}
897 inventory_displayhints = {}
898 # For each view a function can be registered that has to return either True
899 # or False to show a view as context link
900 view_is_enabled = {}
903 def view_title(view):
904 return visuals.visual_title('view', view)
907 def transform_action_url(url_spec):
908 if isinstance(url_spec, tuple):
909 return url_spec
910 return (url_spec, None)
913 def is_stale(row):
914 return row.get('service_staleness', row.get('host_staleness', 0)) >= config.staleness_threshold
917 def paint_stalified(row, text):
918 if is_stale(row):
919 return "stale", text
920 return "", text
923 def paint_host_list(site, hosts):
924 return "", ", ".join(cmk.gui.view_utils.get_host_list_links(site, hosts))
927 def format_plugin_output(output, row):
928 return cmk.gui.view_utils.format_plugin_output(
929 output, row, shall_escape=config.escape_plugin_output)
932 def link_to_view(content, row, view_name):
933 if display_options.disabled(display_options.I):
934 return content
936 url = url_to_view(row, view_name)
937 if url:
938 return html.render_a(content, href=url)
939 return content
942 # TODO: There is duplicated logic with visuals.collect_context_links_of()
943 def url_to_view(row, view_name):
944 if display_options.disabled(display_options.I):
945 return None
947 view = get_permitted_views().get(view_name)
948 if view:
949 # Get the context type of the view to link to, then get the parameters of this
950 # context type and try to construct the context from the data of the row
951 url_vars = []
952 datasource = data_source_registry[view['datasource']]()
953 for info_key in datasource.infos:
954 if info_key in view['single_infos']:
955 # Determine which filters (their names) need to be set
956 # for specifying in order to select correct context for the
957 # target view.
958 for filter_name in visuals.info_params(info_key):
959 filter_object = visuals.get_filter(filter_name)
960 # Get the list of URI vars to be set for that filter
961 new_vars = filter_object.variable_settings(row)
962 url_vars += new_vars
964 # See get_link_filter_names() comment for details
965 for src_key, dst_key in visuals.get_link_filter_names(view, datasource.infos,
966 datasource.link_filters):
967 try:
968 url_vars += visuals.get_filter(src_key).variable_settings(row)
969 except KeyError:
970 pass
972 try:
973 url_vars += visuals.get_filter(dst_key).variable_settings(row)
974 except KeyError:
975 pass
977 add_site_hint = visuals.may_add_site_hint(
978 view_name,
979 info_keys=datasource.infos,
980 single_info_keys=view["single_infos"],
981 filter_names=dict(url_vars).keys())
982 if add_site_hint and row.get('site'):
983 url_vars.append(('site', row['site']))
985 do = html.request.var("display_options")
986 if do:
987 url_vars.append(("display_options", do))
989 filename = "mobile_view.py" if html.mobile else "view.py"
990 return filename + "?" + html.urlencode_vars([("view_name", view_name)] + url_vars)
993 def get_tag_groups(row, what):
994 # Sites with old versions that don't have the tag groups column return
995 # None for this field. Convert this to the default value
996 return row.get("%s_tags" % what, {}) or {}
999 def get_labels(row, what):
1000 # Sites with old versions that don't have the labels column return
1001 # None for this field. Convert this to the default value
1002 return row.get("%s_labels" % what, {}) or {}
1005 def get_label_sources(row, what):
1006 # Sites with old versions that don't have the sources column return
1007 # None for this field. Convert this to the default value
1008 return row.get("%s_sources" % what, {}) or {}
1011 def get_graph_timerange_from_painter_options():
1012 painter_options = PainterOptions.get_instance()
1013 value = painter_options.get("pnp_timerange")
1014 vs = painter_options.get_valuespec_of("pnp_timerange")
1015 return map(int, vs.compute_range(value)[0])
1018 def paint_age(timestamp, has_been_checked, bold_if_younger_than, mode=None, what='past'):
1019 if not has_been_checked:
1020 return "age", "-"
1022 painter_options = PainterOptions.get_instance()
1023 if mode is None:
1024 mode = painter_options.get("ts_format")
1026 if mode == "epoch":
1027 return "", str(int(timestamp))
1029 if mode == "both":
1030 css, h1 = paint_age(timestamp, has_been_checked, bold_if_younger_than, "abs", what=what)
1031 css, h2 = paint_age(timestamp, has_been_checked, bold_if_younger_than, "rel", what=what)
1032 return css, "%s - %s" % (h1, h2)
1034 dateformat = painter_options.get("ts_date")
1035 age = time.time() - timestamp
1036 if mode == "abs" or (mode == "mixed" and abs(age) >= 48 * 3600):
1037 return "age", time.strftime(dateformat + " %H:%M:%S", time.localtime(timestamp))
1039 warn_txt = ''
1040 output_format = "%s"
1041 if what == 'future' and age > 0:
1042 warn_txt = ' <b>%s</b>' % _('in the past!')
1043 elif what == 'past' and age < 0:
1044 warn_txt = ' <b>%s</b>' % _('in the future!')
1045 elif what == 'both' and age > 0:
1046 output_format = "%%s %s" % _("ago")
1048 # Time delta less than two days => make relative time
1049 if age < 0:
1050 age = -age
1051 prefix = "in "
1052 else:
1053 prefix = ""
1054 if age < bold_if_younger_than:
1055 age_class = "age recent"
1056 else:
1057 age_class = "age"
1059 return age_class, prefix + (output_format % cmk.utils.render.approx_age(age)) + warn_txt
1062 def paint_nagiosflag(row, field, bold_if_nonzero):
1063 value = row[field]
1064 yesno = {True: _("yes"), False: _("no")}[value != 0]
1065 if (value != 0) == bold_if_nonzero:
1066 return "badflag", yesno
1067 return "goodflag", yesno
1070 def declare_simple_sorter(name, title, column, func):
1071 register_sorter(name, {
1072 "title": title,
1073 "columns": [column],
1074 "cmp": lambda self, r1, r2: func(column, r1, r2)
1078 def declare_1to1_sorter(painter_name, func, col_num=0, reverse=False):
1079 painter = painter_registry[painter_name]()
1081 if not reverse:
1082 cmp_func = lambda self, r1, r2: func(painter.columns[col_num], r1, r2)
1083 else:
1084 cmp_func = lambda self, r1, r2: func(painter.columns[col_num], r2, r1)
1086 register_sorter(painter_name, {
1087 "title": painter.title,
1088 "columns": painter.columns,
1089 "cmp": cmp_func,
1091 return painter_name
1094 def cmp_simple_number(column, r1, r2):
1095 return cmp(r1.get(column), r2.get(column))
1098 def cmp_num_split(column, r1, r2):
1099 return cmk.gui.utils.cmp_num_split(r1[column].lower(), r2[column].lower())
1102 def cmp_simple_string(column, r1, r2):
1103 v1, v2 = r1.get(column, ''), r2.get(column, '')
1104 return cmp_insensitive_string(v1, v2)
1107 def cmp_insensitive_string(v1, v2):
1108 c = cmp(v1.lower(), v2.lower())
1109 # force a strict order in case of equal spelling but different
1110 # case!
1111 if c == 0:
1112 return cmp(v1, v2)
1113 return c
1116 def cmp_string_list(column, r1, r2):
1117 v1 = ''.join(r1.get(column, []))
1118 v2 = ''.join(r2.get(column, []))
1119 return cmp_insensitive_string(v1, v2)
1122 def cmp_service_name_equiv(r):
1123 if r == "Check_MK":
1124 return -6
1125 elif r == "Check_MK Agent":
1126 return -5
1127 elif r == "Check_MK Discovery":
1128 return -4
1129 elif r == "Check_MK inventory":
1130 return -3 # FIXME: Remove old name one day
1131 elif r == "Check_MK HW/SW Inventory":
1132 return -2
1133 return 0
1136 def cmp_custom_variable(r1, r2, key, cmp_func):
1137 return cmp(get_custom_var(r1, key), get_custom_var(r2, key))
1140 def cmp_ip_address(column, r1, r2):
1141 def split_ip(ip):
1142 try:
1143 return tuple(int(part) for part in ip.split('.'))
1144 except:
1145 return ip
1147 v1, v2 = split_ip(r1.get(column, '')), split_ip(r2.get(column, ''))
1148 return cmp(v1, v2)
1151 def get_custom_var(row, key):
1152 return row["custom_variables"].get(key, "")
1155 def get_perfdata_nth_value(row, n, remove_unit=False):
1156 perfdata = row.get("service_perf_data")
1157 if not perfdata:
1158 return ''
1159 try:
1160 parts = perfdata.split()
1161 if len(parts) <= n:
1162 return "" # too few values in perfdata
1163 _varname, rest = parts[n].split("=")
1164 number = rest.split(';')[0]
1165 # Remove unit. Why should we? In case of sorter (numeric)
1166 if remove_unit:
1167 while len(number) > 0 and not number[-1].isdigit():
1168 number = number[:-1]
1169 return number
1170 except Exception as e:
1171 return str(e)
1174 # Retrieve data via livestatus, convert into list of dicts,
1175 # prepare row-function needed for painters
1176 # datasource: the datasource object as defined in plugins/views/datasources.py
1177 # columns: the list of livestatus columns to query
1178 # add_headers: additional livestatus headers to add
1179 # only_sites: list of sites the query is limited to
1180 # limit: maximum number of data rows to query
1181 def query_data(datasource, columns, add_headers, only_sites=None, limit=None, tablename=None):
1182 if only_sites is None:
1183 only_sites = []
1185 if tablename is None:
1186 tablename = datasource.table
1188 add_headers += datasource.add_headers
1189 merge_column = datasource.merge_by
1190 if merge_column:
1191 columns = [merge_column] + columns
1193 # Most layouts need current state of object in order to
1194 # choose background color - even if no painter for state
1195 # is selected. Make sure those columns are fetched. This
1196 # must not be done for the table 'log' as it cannot correctly
1197 # distinguish between service_state and host_state
1198 if "log" not in datasource.infos:
1199 state_columns = []
1200 if "service" in datasource.infos:
1201 state_columns += ["service_has_been_checked", "service_state"]
1202 if "host" in datasource.infos:
1203 state_columns += ["host_has_been_checked", "host_state"]
1204 for c in state_columns:
1205 if c not in columns:
1206 columns.append(c)
1208 # Remove columns which are implicitely added by the datasource
1209 columns = [c for c in columns if c not in datasource.add_columns]
1210 query = "GET %s\n" % tablename
1211 rows = do_query_data(query, columns, datasource.add_columns, merge_column, add_headers,
1212 only_sites, limit, datasource.auth_domain)
1214 return datasource.post_process(rows)
1217 def do_query_data(query, columns, add_columns, merge_column, add_headers, only_sites, limit,
1218 auth_domain):
1219 query += "Columns: %s\n" % " ".join(columns)
1220 query += add_headers
1221 sites.live().set_prepend_site(True)
1223 if limit is not None:
1224 sites.live().set_limit(limit + 1) # + 1: We need to know, if limit is exceeded
1225 else:
1226 sites.live().set_limit(None)
1228 if config.debug_livestatus_queries \
1229 and html.output_format == "html" and display_options.enabled(display_options.W):
1230 html.open_div(class_=["livestatus", "message"])
1231 html.tt(query.replace('\n', '<br>\n'))
1232 html.close_div()
1234 if only_sites:
1235 sites.live().set_only_sites(only_sites)
1236 sites.live().set_auth_domain(auth_domain)
1237 data = sites.live().query(query)
1238 sites.live().set_auth_domain("read")
1239 sites.live().set_only_sites(None)
1240 sites.live().set_prepend_site(False)
1241 sites.live().set_limit() # removes limit
1243 if merge_column:
1244 data = _merge_data(data, columns)
1246 # convert lists-rows into dictionaries.
1247 # performance, but makes live much easier later.
1248 columns = ["site"] + columns + add_columns
1249 rows = [dict(zip(columns, row)) for row in data]
1251 return rows
1254 # Merge all data rows with different sites but the same value
1255 # in merge_column. We require that all column names are prefixed
1256 # with the tablename. The column with the merge key is required
1257 # to be the *second* column (right after the site column)
1258 def _merge_data(data, columns):
1259 merged = {}
1260 mergefuncs = [lambda a, b: ""] # site column is not merged
1262 def worst_service_state(a, b):
1263 if a == 2 or b == 2:
1264 return 2
1265 return max(a, b)
1267 def worst_host_state(a, b):
1268 if a == 1 or b == 1:
1269 return 1
1270 return max(a, b)
1272 for c in columns:
1273 _tablename, col = c.split("_", 1)
1274 if col.startswith("num_") or col.startswith("members"):
1275 mergefunc = lambda a, b: a + b
1276 elif col.startswith("worst_service"):
1277 return worst_service_state
1278 elif col.startswith("worst_host"):
1279 return worst_host_state
1280 else:
1281 mergefunc = lambda a, b: a
1282 mergefuncs.append(mergefunc)
1284 for row in data:
1285 mergekey = row[1]
1286 if mergekey in merged:
1287 oldrow = merged[mergekey]
1288 merged[mergekey] = [f(a, b) for f, a, b in zip(mergefuncs, oldrow, row)]
1289 else:
1290 merged[mergekey] = row
1292 # return all rows sorted according to merge key
1293 mergekeys = merged.keys()
1294 mergekeys.sort()
1295 return [merged[k] for k in mergekeys]
1298 def join_row(row, cell):
1299 if isinstance(cell, JoinCell):
1300 return row.get("JOIN", {}).get(cell.join_service())
1301 return row
1304 def get_view_infos(view):
1305 """Return list of available datasources (used to render filters)"""
1306 ds_name = view.get('datasource', html.request.var('datasource'))
1307 return data_source_registry[ds_name]().infos
1310 def replace_action_url_macros(url, what, row):
1311 macros = {
1312 "HOSTNAME": row['host_name'],
1313 "HOSTADDRESS": row['host_address'],
1314 "USER_ID": config.user.id,
1316 if what == 'service':
1317 macros.update({
1318 "SERVICEDESC": row['service_description'],
1321 for key, val in macros.items():
1322 url = url.replace("$%s$" % key, val)
1323 url = url.replace("$%s_URL_ENCODED$" % key, html.urlencode(val))
1325 return url
1328 # Intelligent Links to PNP4Nagios 0.6.X
1329 def pnp_url(row, what, how='graph'):
1330 sitename = row["site"]
1331 host = cmk.utils.pnp_cleanup(row["host_name"])
1332 if what == "host":
1333 svc = "_HOST_"
1334 else:
1335 svc = cmk.utils.pnp_cleanup(row["service_description"])
1336 url_prefix = config.site(sitename)["url_prefix"]
1337 if html.mobile:
1338 url = url_prefix + ("pnp4nagios/index.php?kohana_uri=/mobile/%s/%s/%s" %
1339 (how, html.urlencode(host), html.urlencode(svc)))
1340 else:
1341 url = url_prefix + ("pnp4nagios/index.php/%s?host=%s&srv=%s" %
1342 (how, html.urlencode(host), html.urlencode(svc)))
1344 pnp_theme = html.get_theme()
1345 if pnp_theme == "classic":
1346 pnp_theme = "multisite"
1348 if how == 'graph':
1349 url += "&theme=%s&baseurl=%scheck_mk/" % (pnp_theme, html.urlencode(url_prefix))
1350 return url
1353 def render_cache_info(what, row):
1354 cached_at = row["service_cached_at"]
1355 cache_interval = row["service_cache_interval"]
1356 cache_age = time.time() - cached_at
1358 text = _("Cache generated %s ago, cache interval: %s") % \
1359 (cmk.utils.render.approx_age(cache_age), cmk.utils.render.approx_age(cache_interval))
1361 if cache_interval:
1362 percentage = 100.0 * cache_age / cache_interval
1363 text += _(", elapsed cache lifespan: %s") % cmk.utils.render.percent(percentage)
1365 return text
1368 class ViewStore(object):
1369 @classmethod
1370 def get_instance(cls):
1371 """Use the request globals to prevent multiple instances during a request"""
1372 if "view_store" not in current_app.g:
1373 current_app.g["view_store"] = cls()
1374 return current_app.g["view_store"]
1376 def __init__(self):
1377 self.all = self._load_all_views()
1378 self.permitted = self._load_permitted_views(self.all)
1380 def _load_all_views(self):
1381 """Loads all view definitions from disk and returns them"""
1382 # Skip views which do not belong to known datasources
1383 return _transform_old_views(
1384 visuals.load(
1385 'views',
1386 multisite_builtin_views,
1387 skip_func=lambda v: v['datasource'] not in data_source_registry))
1389 def _load_permitted_views(self, all_views):
1390 """Returns all view defitions that a user is allowed to use"""
1391 return visuals.available('views', all_views)
1394 def get_all_views():
1395 return ViewStore.get_instance().all
1398 def get_permitted_views():
1399 return ViewStore.get_instance().permitted
1402 # Convert views that are saved in the pre 1.2.6-style
1403 # FIXME: Can be removed one day. Mark as incompatible change or similar.
1404 def _transform_old_views(all_views):
1405 for view in all_views.values():
1406 ds_name = view['datasource']
1407 datasource = data_source_registry[ds_name]()
1409 if "context" not in view: # legacy views did not have this explicitly
1410 view.setdefault("user_sortable", True)
1412 if 'context_type' in view:
1413 # This code transforms views from user_views.mk which have been migrated with
1414 # daily snapshots from 2014-08 till beginning 2014-10.
1415 visuals.transform_old_visual(view)
1417 elif 'single_infos' not in view:
1418 # This tries to map the datasource and additional settings of the
1419 # views to get the correct view context
1421 # This code transforms views from views.mk (legacy format) to the current format
1422 try:
1423 hide_filters = view.get('hide_filters')
1425 if 'service' in hide_filters and 'host' in hide_filters:
1426 view['single_infos'] = ['service', 'host']
1427 elif 'service' in hide_filters and 'host' not in hide_filters:
1428 view['single_infos'] = ['service']
1429 elif 'host' in hide_filters:
1430 view['single_infos'] = ['host']
1431 elif 'hostgroup' in hide_filters:
1432 view['single_infos'] = ['hostgroup']
1433 elif 'servicegroup' in hide_filters:
1434 view['single_infos'] = ['servicegroup']
1435 elif 'aggr_service' in hide_filters:
1436 view['single_infos'] = ['service']
1437 elif 'aggr_name' in hide_filters:
1438 view['single_infos'] = ['aggr']
1439 elif 'aggr_group' in hide_filters:
1440 view['single_infos'] = ['aggr_group']
1441 elif 'log_contact_name' in hide_filters:
1442 view['single_infos'] = ['contact']
1443 elif 'event_host' in hide_filters:
1444 view['single_infos'] = ['host']
1445 elif hide_filters == ['event_id', 'history_line']:
1446 view['single_infos'] = ['history']
1447 elif 'event_id' in hide_filters:
1448 view['single_infos'] = ['event']
1449 elif 'aggr_hosts' in hide_filters:
1450 view['single_infos'] = ['host']
1451 else:
1452 # For all other context types assume the view is showing multiple objects
1453 # and the datasource can simply be gathered from the datasource
1454 view['single_infos'] = []
1455 except: # Exceptions can happen for views saved with certain GIT versions
1456 if config.debug:
1457 raise
1459 # Convert from show_filters, hide_filters, hard_filters and hard_filtervars
1460 # to context construct
1461 if 'context' not in view:
1462 view[
1463 'show_filters'] = view['hide_filters'] + view['hard_filters'] + view['show_filters']
1465 single_keys = visuals.get_single_info_keys(view)
1467 # First get vars for the classic filters
1468 context = {}
1469 filtervars = dict(view['hard_filtervars'])
1470 all_vars = {}
1471 for filter_name in view['show_filters']:
1472 if filter_name in single_keys:
1473 continue # skip conflictings vars / filters
1475 context.setdefault(filter_name, {})
1476 try:
1477 f = visuals.get_filter(filter_name)
1478 except:
1479 # The exact match filters have been removed. They where used only as
1480 # link filters anyway - at least by the builtin views.
1481 continue
1483 for var in f.htmlvars:
1484 # Check whether or not the filter is supported by the datasource,
1485 # then either skip or use the filter vars
1486 if var in filtervars and f.info in datasource.infos:
1487 value = filtervars[var]
1488 all_vars[var] = value
1489 context[filter_name][var] = value
1491 # We changed different filters since the visuals-rewrite. This must be treated here, since
1492 # we need to transform views which have been created with the old filter var names.
1493 # Changes which have been made so far:
1494 changed_filter_vars = {
1495 'serviceregex': { # Name of the filter
1496 # old var name: new var name
1497 'service': 'service_regex',
1499 'hostregex': {
1500 'host': 'host_regex',
1502 'hostgroupnameregex': {
1503 'hostgroup_name': 'hostgroup_regex',
1505 'servicegroupnameregex': {
1506 'servicegroup_name': 'servicegroup_regex',
1508 'opthostgroup': {
1509 'opthostgroup': 'opthost_group',
1510 'neg_opthostgroup': 'neg_opthost_group',
1512 'optservicegroup': {
1513 'optservicegroup': 'optservice_group',
1514 'neg_optservicegroup': 'neg_optservice_group',
1516 'hostgroup': {
1517 'hostgroup': 'host_group',
1518 'neg_hostgroup': 'neg_host_group',
1520 'servicegroup': {
1521 'servicegroup': 'service_group',
1522 'neg_servicegroup': 'neg_service_group',
1524 'host_contactgroup': {
1525 'host_contactgroup': 'host_contact_group',
1526 'neg_host_contactgroup': 'neg_host_contact_group',
1528 'service_contactgroup': {
1529 'service_contactgroup': 'service_contact_group',
1530 'neg_service_contactgroup': 'neg_service_contact_group',
1534 if filter_name in changed_filter_vars and f.info in datasource.infos:
1535 for old_var, new_var in changed_filter_vars[filter_name].items():
1536 if old_var in filtervars:
1537 value = filtervars[old_var]
1538 all_vars[new_var] = value
1539 context[filter_name][new_var] = value
1541 # Now, when there are single object infos specified, add these keys to the
1542 # context
1543 for single_key in single_keys:
1544 if single_key in all_vars:
1545 context[single_key] = all_vars[single_key]
1547 view['context'] = context
1549 # Cleanup unused attributes
1550 for k in ['hide_filters', 'hard_filters', 'show_filters', 'hard_filtervars']:
1551 try:
1552 del view[k]
1553 except KeyError:
1554 pass
1556 return all_views
1560 # .--Cells---------------------------------------------------------------.
1561 # | ____ _ _ |
1562 # | / ___|___| | |___ |
1563 # | | | / _ \ | / __| |
1564 # | | |__| __/ | \__ \ |
1565 # | \____\___|_|_|___/ |
1566 # | |
1567 # +----------------------------------------------------------------------+
1568 # | View cell handling classes. Each cell instanciates a multisite |
1569 # | painter to render a table cell. |
1570 # '----------------------------------------------------------------------'
1573 def extract_painter_name(painter_spec):
1574 if isinstance(painter_spec[0], tuple):
1575 return painter_spec[0][0]
1576 if isinstance(painter_spec, tuple):
1577 return painter_spec[0]
1578 if isinstance(painter_spec, six.string_types):
1579 return painter_spec
1582 def painter_exists(painter_spec):
1583 painter_name = extract_painter_name(painter_spec)
1585 return painter_name in painter_registry
1588 class Cell(object):
1589 """A cell is an instance of a painter in a view (-> a cell or a grouping cell)"""
1591 # Wanted to have the "parse painter spec logic" in one place (The Cell() class)
1592 # but this should be cleaned up more. TODO: Move this to another place
1593 @staticmethod
1594 def is_join_cell(painter_spec):
1595 return len(painter_spec) >= 4
1597 def __init__(self, view, painter_spec=None):
1598 self._view = view
1599 self._painter_name = None
1600 self._painter_params = None
1601 self._link_view_name = None
1602 self._tooltip_painter_name = None
1604 if painter_spec:
1605 self._from_view(painter_spec)
1607 # In views the painters are saved as tuples of the following formats:
1609 # Painter name, Link view name
1610 # ('service_discovery_service', None),
1612 # Painter name, Link view name, Hover painter name
1613 # ('host_plugin_output', None, None),
1615 # Join column: Painter name, Link view name, hover painter name, Join service description
1616 # ('service_description', None, None, u'CPU load')
1618 # Join column: Painter name, Link view name, hover painter name, Join service description, custom title
1619 # ('service_description', None, None, u'CPU load')
1621 # Parameterized painters:
1622 # Same as above but instead of the "Painter name" a two element tuple with the painter name as
1623 # first element and a dictionary of parameters as second element is set.
1624 def _from_view(self, painter_spec):
1625 self._painter_name = extract_painter_name(painter_spec)
1626 if isinstance(painter_spec[0], tuple):
1627 self._painter_params = painter_spec[0][1]
1629 if painter_spec[1] is not None:
1630 self._link_view_name = painter_spec[1]
1632 if len(painter_spec) >= 3 and painter_spec[2] in painter_registry:
1633 self._tooltip_painter_name = painter_spec[2]
1635 # Get a list of columns we need to fetch in order to render this cell
1636 def needed_columns(self):
1637 columns = set(self.painter().columns)
1639 if self._link_view_name:
1640 if self._has_link():
1641 link_view = self._link_view()
1642 if link_view:
1643 # TODO: Clean this up here
1644 for filt in [
1645 visuals.get_filter(fn) for fn in visuals.get_single_info_keys(link_view)
1647 columns.update(filt.link_columns)
1649 if self.has_tooltip():
1650 columns.update(self.tooltip_painter().columns)
1652 return columns
1654 def is_joined(self):
1655 return False
1657 def join_service(self):
1658 return None
1660 def _has_link(self):
1661 return self._link_view_name is not None
1663 def _link_view(self):
1664 try:
1665 return get_permitted_views()[self._link_view_name]
1666 except KeyError:
1667 return None
1669 def painter(self):
1670 return painter_registry[self._painter_name]()
1672 def painter_name(self):
1673 return self._painter_name
1675 def export_title(self):
1676 return self._painter_name
1678 def painter_options(self):
1679 return self.painter().painter_options
1681 # The parameters configured in the view for this painter. In case the
1682 # painter has params, it defaults to the valuespec default value and
1683 # in case the painter has no params, it returns None.
1684 def painter_parameters(self):
1685 vs_painter_params = self.painter().parameters
1686 if not vs_painter_params:
1687 return
1689 if self._painter_params is None:
1690 return vs_painter_params.default_value()
1692 return self._painter_params
1694 def title(self, use_short=True):
1695 painter = self.painter()
1696 if use_short:
1697 return self._get_short_title(painter)
1698 return self._get_long_title(painter)
1700 def _get_short_title(self, painter):
1701 # TODO: Hack for the SLA painters. Find a better way
1702 if callable(painter.short_title):
1703 return painter.short_title(self.painter_parameters())
1704 return painter.short_title
1706 def _get_long_title(self, painter):
1707 # TODO: Hack for the SLA painters. Find a better way
1708 if callable(painter.title):
1709 return painter.title(self.painter_parameters())
1710 return painter.title
1712 # Can either be:
1713 # True : Is printable in PDF
1714 # False : Is not printable at all
1715 # "<string>" : ID of a painter_printer (Reporting module)
1716 def printable(self):
1717 return self.painter().printable
1719 def has_tooltip(self):
1720 return self._tooltip_painter_name is not None
1722 def tooltip_painter_name(self):
1723 return self._tooltip_painter_name
1725 def tooltip_painter(self):
1726 return painter_registry[self._tooltip_painter_name]()
1728 def paint_as_header(self, is_last_column_header=False):
1729 # Optional: Sort link in title cell
1730 # Use explicit defined sorter or implicit the sorter with the painter name
1731 # Important for links:
1732 # - Add the display options (Keeping the same display options as current)
1733 # - Link to _self (Always link to the current frame)
1734 classes = []
1735 onclick = ''
1736 title = ''
1737 if display_options.enabled(display_options.L) \
1738 and self._view.spec.get('user_sortable', False) \
1739 and _get_sorter_name_of_painter(self.painter_name()) is not None:
1740 params = [
1741 ('sort', self._sort_url()),
1743 if display_options.title_options:
1744 params.append(('display_options', display_options.title_options))
1746 classes += ["sort", _get_primary_sorter_order(self._view, self.painter_name())]
1747 onclick = "location.href=\'%s\'" % html.makeuri(params, 'sort')
1748 title = _('Sort by %s') % self.title()
1750 if is_last_column_header:
1751 classes.append("last_col")
1753 html.open_th(class_=classes, onclick=onclick, title=title)
1754 html.write(self.title())
1755 html.close_th()
1756 #html.guitest_record_output("view", ("header", title))
1758 def _sort_url(self):
1760 The following sorters need to be handled in this order:
1762 1. group by sorter (needed in grouped views)
1763 2. user defined sorters (url sorter)
1764 3. configured view sorters
1766 sorter = []
1768 group_sort, user_sort, view_sort = _get_separated_sorters(self._view)
1770 sorter = group_sort + user_sort + view_sort
1772 # Now apply the sorter of the current column:
1773 # - Negate/Disable when at first position
1774 # - Move to the first position when already in sorters
1775 # - Add in the front of the user sorters when not set
1776 sorter_name = _get_sorter_name_of_painter(self.painter_name())
1777 if self.is_joined():
1778 # TODO: Clean this up and then remove Cell.join_service()
1779 this_asc_sorter = (sorter_name, False, self.join_service())
1780 this_desc_sorter = (sorter_name, True, self.join_service())
1781 else:
1782 this_asc_sorter = (sorter_name, False)
1783 this_desc_sorter = (sorter_name, True)
1785 if user_sort and this_asc_sorter == user_sort[0]:
1786 # Second click: Change from asc to desc order
1787 sorter[sorter.index(this_asc_sorter)] = this_desc_sorter
1789 elif user_sort and this_desc_sorter == user_sort[0]:
1790 # Third click: Remove this sorter
1791 sorter.remove(this_desc_sorter)
1793 else:
1794 # First click: add this sorter as primary user sorter
1795 # Maybe the sorter is already in the user sorters or view sorters, remove it
1796 for s in [user_sort, view_sort]:
1797 if this_asc_sorter in s:
1798 s.remove(this_asc_sorter)
1799 if this_desc_sorter in s:
1800 s.remove(this_desc_sorter)
1801 # Now add the sorter as primary user sorter
1802 sorter = group_sort + [this_asc_sorter] + user_sort + view_sort
1804 p = []
1805 for s in sorter:
1806 if len(s) == 2:
1807 p.append((s[1] and '-' or '') + s[0])
1808 else:
1809 p.append((s[1] and '-' or '') + s[0] + '~' + s[2])
1811 return ','.join(p)
1813 def render(self, row):
1814 row = join_row(row, self)
1816 try:
1817 tdclass, content = self.render_content(row)
1818 except:
1819 logger.exception("Failed to render painter '%s' (Row: %r)" % (self._painter_name, row))
1820 raise
1822 if tdclass is None:
1823 tdclass = ""
1825 if tdclass == "" and content == "":
1826 return "", ""
1828 # Add the optional link to another view
1829 if content and self._has_link():
1830 content = link_to_view(content, row, self._link_view_name)
1832 # Add the optional mouseover tooltip
1833 if content and self.has_tooltip():
1834 tooltip_cell = Cell(self._view, (self.tooltip_painter_name(), None))
1835 _tooltip_tdclass, tooltip_content = tooltip_cell.render_content(row)
1836 tooltip_text = html.strip_tags(tooltip_content)
1837 content = '<span title="%s">%s</span>' % (tooltip_text, content)
1839 return tdclass, content
1841 # Same as self.render() for HTML output: Gets a painter and a data
1842 # row and creates the text for being painted.
1843 def render_for_pdf(self, row, time_range):
1844 # TODO: Move this somewhere else!
1845 def find_htdocs_image_path(filename):
1846 dirs = [
1847 cmk.utils.paths.local_web_dir + "/htdocs/",
1848 cmk.utils.paths.web_dir + "/htdocs/",
1850 for d in dirs:
1851 if os.path.exists(d + filename):
1852 return d + filename
1854 try:
1855 row = join_row(row, self)
1856 css_classes, txt = self.render_content(row)
1857 if txt is None:
1858 return css_classes, ""
1859 txt = txt.strip()
1861 # Handle <img...>. Our PDF writer cannot draw arbitrary
1862 # images, but all that we need for showing simple icons.
1863 # Current limitation: *one* image
1864 if txt.lower().startswith("<img"):
1865 img_filename = re.sub('.*src=["\']([^\'"]*)["\'].*', "\\1", str(txt))
1866 img_path = find_htdocs_image_path(img_filename)
1867 if img_path:
1868 txt = ("icon", img_path)
1869 else:
1870 txt = img_filename
1872 if isinstance(txt, HTML):
1873 txt = html.strip_tags("%s" % txt)
1875 elif not isinstance(txt, tuple):
1876 txt = html.escaper.unescape_attributes(txt)
1877 txt = html.strip_tags(txt)
1879 return css_classes, txt
1880 except Exception:
1881 raise MKGeneralException(
1882 'Failed to paint "%s": %s' % (self.painter_name(), traceback.format_exc()))
1884 def render_content(self, row):
1885 if not row:
1886 return "", "" # nothing to paint
1888 painter = self.painter()
1889 result = painter.render(row, self)
1890 if not isinstance(result, tuple) or len(result) != 2:
1891 raise Exception(_("Painter %r returned invalid result: %r") % (painter.ident, result))
1892 return result
1894 def paint(self, row, tdattrs="", is_last_cell=False):
1895 tdclass, content = self.render(row)
1896 has_content = content != ""
1898 if is_last_cell:
1899 if tdclass is None:
1900 tdclass = "last_col"
1901 else:
1902 tdclass += " last_col"
1904 if tdclass:
1905 html.write("<td %s class=\"%s\">" % (tdattrs, tdclass))
1906 html.write(content)
1907 html.close_td()
1908 else:
1909 html.write("<td %s>" % (tdattrs))
1910 html.write(content)
1911 html.close_td()
1913 return has_content
1916 class JoinCell(Cell):
1917 def __init__(self, view, painter_spec):
1918 self._join_service_descr = None
1919 self._custom_title = None
1920 super(JoinCell, self).__init__(view, painter_spec)
1922 def _from_view(self, painter_spec):
1923 super(JoinCell, self)._from_view(painter_spec)
1925 if len(painter_spec) >= 4:
1926 self._join_service_descr = painter_spec[3]
1928 if len(painter_spec) == 5:
1929 self._custom_title = painter_spec[4]
1931 def is_joined(self):
1932 return True
1934 def join_service(self):
1935 return self._join_service_descr
1937 def livestatus_filter(self, join_column_name):
1938 return "Filter: %s = %s" % \
1939 (livestatus.lqencode(join_column_name), livestatus.lqencode(self._join_service_descr))
1941 def title(self, use_short=True):
1942 if self._custom_title:
1943 return self._custom_title
1944 return self._join_service_descr
1946 def export_title(self):
1947 return "%s.%s" % (self._painter_name, self.join_service())
1950 class EmptyCell(Cell):
1951 def render(self, row):
1952 return "", ""
1954 def paint(self, row, tdattrs="", is_last_cell=False):
1955 return False
1958 def output_csv_headers(view):
1959 filename = '%s-%s.csv' % (view['name'],
1960 time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime(time.time())))
1961 if isinstance(filename, unicode):
1962 filename = filename.encode("utf-8")
1963 html.response.headers["Content-Disposition"] = "Attachment; filename=\"%s\"" % filename
1966 def _get_sorter_name_of_painter(painter_name_or_spec):
1967 painter_name = extract_painter_name(painter_name_or_spec)
1968 painter = painter_registry[painter_name]()
1969 if painter.sorter:
1970 return painter.sorter
1972 if painter_name in sorter_registry:
1973 return painter_name
1975 return None
1978 def _get_separated_sorters(view):
1979 group_sort = [(_get_sorter_name_of_painter(p), False)
1980 for p in view.spec['group_painters']
1981 if painter_exists(p) and _get_sorter_name_of_painter(p) is not None]
1982 view_sort = [s for s in view.spec['sorters'] if not s[0] in group_sort]
1984 user_sort = view.user_sorters
1986 _substract_sorters(user_sort, group_sort)
1987 _substract_sorters(view_sort, user_sort)
1989 return group_sort, user_sort, view_sort
1992 def _get_primary_sorter_order(view, painter_name):
1993 sorter_name = _get_sorter_name_of_painter(painter_name)
1994 this_asc_sorter = (sorter_name, False)
1995 this_desc_sorter = (sorter_name, True)
1996 _group_sort, user_sort, _view_sort = _get_separated_sorters(view)
1997 if user_sort and this_asc_sorter == user_sort[0]:
1998 return 'asc'
1999 elif user_sort and this_desc_sorter == user_sort[0]:
2000 return 'desc'
2001 return ''
2004 def _substract_sorters(base, remove):
2005 for s in remove:
2006 if s in base:
2007 base.remove(s)
2008 elif (s[0], not s[1]) in base:
2009 base.remove((s[0], not s[1]))