2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
26 """Module to hold shared code for internals and the plugins"""
28 # TODO: More feature related splitting up would be better
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
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
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
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.
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"]
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
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
):
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"))
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():
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():
129 if html
.request
.has_var("_reset_painter_options"):
130 self
._clear
_painter
_options
(view
.name
)
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
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
:
149 self
.set(option_name
, value
)
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
157 for name
in painter_option_registry
.keys():
159 del self
._options
[name
]
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):
188 dflt
= self
.get_valuespec_of(name
).default_value()
190 # Some view options (that are not declared as display options)
191 # like "refresh" don't have a valuespec. So they need to default
193 # TODO: Find all occurences and simply declare them as "invisible"
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
)
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():
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
))
231 html
.button("_update_painter_options", _("Submit"), "submit")
232 html
.button("_reset_painter_options", _("Reset"), "submit")
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
):
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
):
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
)
260 for c
in painter
.columns
:
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())])
275 class PainterOption(object):
276 __metaclass__
= abc
.ABCMeta
278 @abc.abstractproperty
281 """The identity of a painter option. One word, may contain alpha numeric characters"""
282 raise NotImplementedError()
284 @abc.abstractproperty
286 # type: () -> ValueSpec
287 raise NotImplementedError()
290 class ViewPainterOptionRegistry(cmk
.utils
.plugin_registry
.ClassRegistry
):
291 def plugin_base_class(self
):
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
307 """The identity of a layout. One word, may contain alpha numeric characters"""
308 raise NotImplementedError()
310 @abc.abstractproperty
313 """Short human readable title of the layout"""
314 raise NotImplementedError()
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
):
325 """Whether this layout can display checkboxes for selecting rows"""
326 raise NotImplementedError()
328 @abc.abstractproperty
331 """Whether this should be hidden from the user (e.g. in the view editor layout choice)"""
332 raise NotImplementedError()
335 def painter_options(self
):
336 # type: () -> List[str]
337 """Returns the painter option identities used by this layout"""
341 def has_individual_csv_export(self
):
343 """Whether this layout has an individual CSV export implementation"""
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"""
352 class ViewLayoutRegistry(cmk
.utils
.plugin_registry
.ClassRegistry
):
353 def plugin_base_class(self
):
356 def plugin_name(self
, plugin_class
):
357 return plugin_class().ident
359 def get_choices(self
):
361 for plugin_class
in self
.values():
362 layout
= plugin_class()
366 choices
.append((layout
.ident
, layout
.title
))
371 layout_registry
= ViewLayoutRegistry()
374 class CommandGroup(object):
375 __metaclass__
= abc
.ABCMeta
377 @abc.abstractproperty
380 """The identity of a command group. One word, may contain alpha numeric characters"""
381 raise NotImplementedError()
383 @abc.abstractproperty
386 raise NotImplementedError()
388 @abc.abstractproperty
389 def sort_index(self
):
391 raise NotImplementedError()
394 class CommandGroupRegistry(cmk
.utils
.plugin_registry
.ClassRegistry
):
395 def plugin_base_class(self
):
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
):
408 "LegacyCommandGroup%s" % ident
.title(), (CommandGroup
,), {
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
425 """The identity of a command. One word, may contain alpha numeric characters"""
426 raise NotImplementedError()
428 @abc.abstractproperty
431 raise NotImplementedError()
433 @abc.abstractproperty
434 def permission(self
):
435 # type: () -> Type[Permission]
436 raise NotImplementedError()
438 @abc.abstractproperty
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()
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()
455 # type: () -> Type[CommandGroup]
456 """The command group the commmand belongs to"""
457 return command_group_registry
["various"]
461 # type: () -> Optional[str]
462 """View name to show a view exclusive command for"""
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
):
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()
486 "LegacyCommand%s" % ident
.title(), (Command
,), {
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"]
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
509 """The identity of a data source. One word, may contain alpha numeric characters"""
510 raise NotImplementedError()
512 @abc.abstractproperty
515 """Used as display-string for the datasource in the GUI (e.g. view editor)"""
516 raise NotImplementedError()
518 @abc.abstractproperty
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
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()
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.
548 def add_columns(self
):
549 # type: () -> List[str]
550 """These columns are requested automatically in addition to the
551 other needed columns."""
555 def add_headers(self
):
557 """additional livestatus headers to add to each call"""
560 @abc.abstractproperty
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
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()
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
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."""
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."""
598 def ignore_limit(self
):
600 """Ignore the soft/hard query limits in view.py/query_data(). This
601 fixes stats queries on e.g. the log table."""
605 def auth_domain(self
):
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
614 def time_filters(self
):
615 # type: () -> List[str]
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"""
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"""
634 class DataSourceLivestatus(DataSource
):
635 """Base class for all simple data sources which 1:1 base on a livestatus table"""
639 return RowTableLivestatus(self
.ident
)
642 class DataSourceRegistry(cmk
.utils
.plugin_registry
.ClassRegistry
):
643 def plugin_base_class(self
):
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
):
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
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
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
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
701 """The identity of a painter. One word, may contain alpha numeric characters"""
702 raise NotImplementedError()
704 @abc.abstractproperty
707 """Used as display string for the painter in the GUI (e.g. view editor)"""
708 raise NotImplementedError()
710 @abc.abstractproperty
712 # type: () -> List[str]
713 """Livestatus columns needed for this painter"""
714 raise NotImplementedError()
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
730 raise NotImplementedError()
733 def short_title(self
):
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"""
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()"""
745 def parameters(self
):
746 # type: () -> Optional[ValueSpec]
747 """Returns either the valuespec of the painter parameters or None"""
751 def painter_options(self
):
752 # type: () -> List[str]
753 """Returns a list of painter option names that affect this painter"""
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)
768 # type: () -> Optional[str]
769 """Returns the optional name of the sorter for this painter"""
772 # TODO: Cleanup this hack
776 """Whether or not to load the HW/SW inventory for this column"""
780 class PainterRegistry(cmk
.utils
.plugin_registry
.ClassRegistry
):
781 def plugin_base_class(self
):
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
):
795 "LegacyPainter%s" % ident
.title(), (Painter
,), {
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
822 """The identity of a sorter. One word, may contain alpha numeric characters"""
823 raise NotImplementedError()
825 @abc.abstractproperty
828 """Used as display string for the sorter in the GUI (e.g. view editor)"""
829 raise NotImplementedError()
831 @abc.abstractproperty
833 # type: () -> List[str]
834 """Livestatus columns needed for this sorter"""
835 raise NotImplementedError()
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
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,
851 raise NotImplementedError()
855 # type: () -> Optional[List]
856 """Optional list of arguments for the cmp function"""
859 # TODO: Cleanup this hack
863 """Whether or not to load the HW/SW inventory for this column"""
867 class SorterRegistry(cmk
.utils
.plugin_registry
.ClassRegistry
):
868 def plugin_base_class(self
):
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
):
882 "LegacySorter%s" % ident
.title(), (Sorter
,), {
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)),
891 sorter_registry
.register(cls
)
894 # TODO: Refactor to plugin_registries
895 multisite_builtin_views
= {}
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
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):
910 return (url_spec
, None)
914 return row
.get('service_staleness', row
.get('host_staleness', 0)) >= config
.staleness_threshold
917 def paint_stalified(row
, 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
):
936 url
= url_to_view(row
, view_name
)
938 return html
.render_a(content
, href
=url
)
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
):
947 view
= get_permitted_views().get(view_name
)
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
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
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
)
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
):
968 url_vars
+= visuals
.get_filter(src_key
).variable_settings(row
)
973 url_vars
+= visuals
.get_filter(dst_key
).variable_settings(row
)
977 add_site_hint
= visuals
.may_add_site_hint(
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")
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
:
1022 painter_options
= PainterOptions
.get_instance()
1024 mode
= painter_options
.get("ts_format")
1027 return "", str(int(timestamp
))
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
))
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
1054 if age
< bold_if_younger_than
:
1055 age_class
= "age recent"
1059 return age_class
, prefix
+ (output_format
% cmk
.utils
.render
.approx_age(age
)) + warn_txt
1062 def paint_nagiosflag(row
, field
, bold_if_nonzero
):
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
, {
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
]()
1082 cmp_func
= lambda self
, r1
, r2
: func(painter
.columns
[col_num
], r1
, r2
)
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
,
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
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
):
1125 elif r
== "Check_MK Agent":
1127 elif r
== "Check_MK Discovery":
1129 elif r
== "Check_MK inventory":
1130 return -3 # FIXME: Remove old name one day
1131 elif r
== "Check_MK HW/SW Inventory":
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
):
1143 return tuple(int(part
) for part
in ip
.split('.'))
1147 v1
, v2
= split_ip(r1
.get(column
, '')), split_ip(r2
.get(column
, ''))
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")
1160 parts
= perfdata
.split()
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)
1167 while len(number
) > 0 and not number
[-1].isdigit():
1168 number
= number
[:-1]
1170 except Exception as 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:
1185 if tablename
is None:
1186 tablename
= datasource
.table
1188 add_headers
+= datasource
.add_headers
1189 merge_column
= datasource
.merge_by
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
:
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
:
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
,
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
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'))
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
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
]
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
):
1260 mergefuncs
= [lambda a
, b
: ""] # site column is not merged
1262 def worst_service_state(a
, b
):
1263 if a
== 2 or b
== 2:
1267 def worst_host_state(a
, b
):
1268 if a
== 1 or b
== 1:
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
1281 mergefunc
= lambda a
, b
: a
1282 mergefuncs
.append(mergefunc
)
1286 if mergekey
in merged
:
1287 oldrow
= merged
[mergekey
]
1288 merged
[mergekey
] = [f(a
, b
) for f
, a
, b
in zip(mergefuncs
, oldrow
, row
)]
1290 merged
[mergekey
] = row
1292 # return all rows sorted according to merge key
1293 mergekeys
= merged
.keys()
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())
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
):
1312 "HOSTNAME": row
['host_name'],
1313 "HOSTADDRESS": row
['host_address'],
1314 "USER_ID": config
.user
.id,
1316 if what
== 'service':
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
))
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"])
1335 svc
= cmk
.utils
.pnp_cleanup(row
["service_description"])
1336 url_prefix
= config
.site(sitename
)["url_prefix"]
1338 url
= url_prefix
+ ("pnp4nagios/index.php?kohana_uri=/mobile/%s/%s/%s" %
1339 (how
, html
.urlencode(host
), html
.urlencode(svc
)))
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"
1349 url
+= "&theme=%s&baseurl=%scheck_mk/" % (pnp_theme
, html
.urlencode(url_prefix
))
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
))
1362 percentage
= 100.0 * cache_age
/ cache_interval
1363 text
+= _(", elapsed cache lifespan: %s") % cmk
.utils
.render
.percent(percentage
)
1368 class ViewStore(object):
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"]
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(
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
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']
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
1459 # Convert from show_filters, hide_filters, hard_filters and hard_filtervars
1460 # to context construct
1461 if 'context' not in 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
1469 filtervars
= dict(view
['hard_filtervars'])
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
, {})
1477 f
= visuals
.get_filter(filter_name
)
1479 # The exact match filters have been removed. They where used only as
1480 # link filters anyway - at least by the builtin views.
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',
1500 'host': 'host_regex',
1502 'hostgroupnameregex': {
1503 'hostgroup_name': 'hostgroup_regex',
1505 'servicegroupnameregex': {
1506 'servicegroup_name': 'servicegroup_regex',
1509 'opthostgroup': 'opthost_group',
1510 'neg_opthostgroup': 'neg_opthost_group',
1512 'optservicegroup': {
1513 'optservicegroup': 'optservice_group',
1514 'neg_optservicegroup': 'neg_optservice_group',
1517 'hostgroup': 'host_group',
1518 'neg_hostgroup': 'neg_host_group',
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
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']:
1560 # .--Cells---------------------------------------------------------------.
1562 # | / ___|___| | |___ |
1563 # | | | / _ \ | / __| |
1564 # | | |__| __/ | \__ \ |
1565 # | \____\___|_|_|___/ |
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
):
1582 def painter_exists(painter_spec
):
1583 painter_name
= extract_painter_name(painter_spec
)
1585 return painter_name
in painter_registry
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
1594 def is_join_cell(painter_spec
):
1595 return len(painter_spec
) >= 4
1597 def __init__(self
, view
, painter_spec
=None):
1599 self
._painter
_name
= None
1600 self
._painter
_params
= None
1601 self
._link
_view
_name
= None
1602 self
._tooltip
_painter
_name
= None
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
()
1643 # TODO: Clean this up here
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
)
1654 def is_joined(self
):
1657 def join_service(self
):
1660 def _has_link(self
):
1661 return self
._link
_view
_name
is not None
1663 def _link_view(self
):
1665 return get_permitted_views()[self
._link
_view
_name
]
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
:
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()
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
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)
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:
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())
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
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())
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
)
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
1807 p
.append((s
[1] and '-' or '') + s
[0])
1809 p
.append((s
[1] and '-' or '') + s
[0] + '~' + s
[2])
1813 def render(self
, row
):
1814 row
= join_row(row
, self
)
1817 tdclass
, content
= self
.render_content(row
)
1819 logger
.exception("Failed to render painter '%s' (Row: %r)" % (self
._painter
_name
, row
))
1825 if tdclass
== "" and content
== "":
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
):
1847 cmk
.utils
.paths
.local_web_dir
+ "/htdocs/",
1848 cmk
.utils
.paths
.web_dir
+ "/htdocs/",
1851 if os
.path
.exists(d
+ filename
):
1855 row
= join_row(row
, self
)
1856 css_classes
, txt
= self
.render_content(row
)
1858 return css_classes
, ""
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
)
1868 txt
= ("icon", img_path
)
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
1881 raise MKGeneralException(
1882 'Failed to paint "%s": %s' % (self
.painter_name(), traceback
.format_exc()))
1884 def render_content(self
, 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
))
1894 def paint(self
, row
, tdattrs
="", is_last_cell
=False):
1895 tdclass
, content
= self
.render(row
)
1896 has_content
= content
!= ""
1900 tdclass
= "last_col"
1902 tdclass
+= " last_col"
1905 html
.write("<td %s class=\"%s\">" % (tdattrs
, tdclass
))
1909 html
.write("<td %s>" % (tdattrs
))
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
):
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
):
1954 def paint(self
, row
, tdattrs
="", is_last_cell
=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
]()
1970 return painter
.sorter
1972 if painter_name
in sorter_registry
:
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]:
1999 elif user_sort
and this_desc_sorter
== user_sort
[0]:
2004 def _substract_sorters(base
, remove
):
2008 elif (s
[0], not s
[1]) in base
:
2009 base
.remove((s
[0], not s
[1]))