Add specific visualization for labels depending on their source
[check_mk.git] / cmk / gui / plugins / views / inventory.py
blob6620af041d79fed22f23d5056ca2524ff07b1f25
1 #!/usr/bin/python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 import time
29 from cmk.utils.regex import regex
30 import cmk.utils.defines as defines
31 import cmk.utils.render
33 import cmk.gui.pages
34 import cmk.gui.config as config
35 import cmk.gui.sites as sites
36 import cmk.gui.utils as utils
37 import cmk.gui.inventory as inventory
38 from cmk.gui.i18n import _
39 from cmk.gui.globals import html, current_app
40 from cmk.gui.htmllib import HTML
41 from cmk.gui.valuespec import Checkbox, Hostname
42 from cmk.gui.exceptions import MKUserError
44 from cmk.gui.plugins.visuals import (
45 filter_registry,
46 VisualInfo,
47 visual_info_registry,
49 from cmk.gui.plugins.visuals.inventory import (
50 FilterInvText,
51 FilterInvBool,
52 FilterInvFloat,
53 FilterInvtableText,
54 FilterInvtableIDRange,
57 from cmk.gui.plugins.views import (
58 data_source_registry,
59 DataSource,
60 RowTable,
61 painter_registry,
62 Painter,
63 register_painter,
64 register_sorter,
65 display_options,
66 painter_option_registry,
67 PainterOption,
68 PainterOptions,
69 inventory_displayhints,
70 multisite_builtin_views,
71 view_is_enabled,
72 paint_age,
73 declare_1to1_sorter,
74 cmp_simple_number,
75 render_labels,
79 def paint_host_inventory_tree(row, invpath=".", column="host_inventory"):
80 struct_tree = row.get(column)
81 if struct_tree is None:
82 return "", ""
84 if column == "host_inventory":
85 painter_options = PainterOptions.get_instance()
86 tree_renderer = AttributeRenderer(
87 row["site"],
88 row["host_name"],
89 "",
90 invpath,
91 show_internal_tree_paths=painter_options.get('show_internal_tree_paths'))
92 else:
93 tree_id = "/" + str(row["invhist_time"])
94 tree_renderer = DeltaNodeRenderer(row["site"], row["host_name"], tree_id, invpath)
96 parsed_path, attributes_key = inventory.parse_tree_path(invpath)
97 if attributes_key is None:
98 return _paint_host_inventory_tree_children(struct_tree, parsed_path, tree_renderer)
99 return _paint_host_inventory_tree_value(struct_tree, parsed_path, tree_renderer, invpath,
100 attributes_key)
103 def _paint_host_inventory_tree_children(struct_tree, parsed_path, tree_renderer):
104 if parsed_path:
105 children = struct_tree.get_sub_children(parsed_path)
106 else:
107 children = [struct_tree.get_root_container()]
108 if children is None:
109 return "", ""
110 with html.plugged():
111 for child in children:
112 child.show(tree_renderer)
113 code = html.drain()
114 return "invtree", code
117 def _paint_host_inventory_tree_value(struct_tree, parsed_path, tree_renderer, invpath,
118 attributes_key):
119 if attributes_key == []:
120 child = struct_tree.get_sub_numeration(parsed_path)
121 else:
122 child = struct_tree.get_sub_attributes(parsed_path)
124 if child is None:
125 return "", ""
127 with html.plugged():
128 if invpath.endswith(".") or invpath.endswith(":"):
129 invpath = invpath[:-1]
130 if attributes_key == []:
131 tree_renderer.show_numeration(child, path=invpath)
132 else:
133 tree_renderer.show_attribute(child.get_child_data().get(attributes_key),
134 _inv_display_hint(invpath))
135 code = html.drain()
136 return "", code
139 def _inv_filter_info():
140 return {
141 "bytes": {
142 "unit": _("MB"),
143 "scale": 1024 * 1024
145 "bytes_rounded": {
146 "unit": _("MB"),
147 "scale": 1024 * 1024
149 "hz": {
150 "unit": _("MHz"),
151 "scale": 1000000
153 "volt": {
154 "unit": _("Volt")
156 "timestamp": {
157 "unit": _("secs")
162 # Declares painters, sorters and filters to be used in views based on all host related datasources.
163 def declare_inv_column(invpath, datatype, title, short=None):
164 if invpath == ".":
165 name = "inv"
166 else:
167 name = "inv_" + invpath.replace(":", "_").replace(".", "_").strip("_")
169 # Declare column painter
170 painter_spec = {
171 "title": invpath == "." and _("Inventory Tree") or (_("Inventory") + ": " + title),
172 "columns": ["host_inventory", "host_structured_status"],
173 "options": ["show_internal_tree_paths"],
174 "load_inv": True,
175 "paint": lambda row: paint_host_inventory_tree(row, invpath),
176 "sorter": name,
178 if short:
179 painter_spec["short"] = short
180 register_painter(name, painter_spec)
182 # Sorters and Filters only for leaf nodes
183 if invpath[-1] not in ":.":
184 # Declare sorter. It will detect numbers automatically
185 register_sorter(
186 name, {
187 "title": _("Inventory") + ": " + title,
188 "columns": ["host_inventory", "host_structured_status"],
189 "load_inv": True,
190 "cmp": lambda a, b: cmp_inventory_node(a, b, invpath),
193 filter_info = _inv_filter_info().get(datatype, {})
195 # Declare filter. Sync this with declare_invtable_columns()
196 if datatype in ["str", "bool"]:
197 parent_class = FilterInvText if datatype == "str" else FilterInvBool
198 filter_class = type(
199 "FilterInv%s" % name.title(), (parent_class,), {
200 "_ident": name,
201 "_title": title,
202 "_inv_path": invpath,
203 "_invpath": property(lambda s: s._inv_path),
204 "sort_index": property(lambda s: 800),
205 "ident": property(lambda s: s._ident),
206 "title": property(lambda s: s._title),
208 else:
209 filter_class = type(
210 "FilterInv%s" % name.title(), (FilterInvFloat,), {
211 "_ident": name,
212 "_title": title,
213 "_inv_path": invpath,
214 "_unit_val": filter_info.get("unit"),
215 "_scale_val": filter_info.get("scale", 1.0),
216 "_unit": property(lambda s: s._unit_val),
217 "_scale": property(lambda s: s._scale_val),
218 "_invpath": property(lambda s: s._inv_path),
219 "sort_index": property(lambda s: 800),
220 "ident": property(lambda s: s._ident),
221 "title": property(lambda s: s._title),
224 filter_registry.register(filter_class)
227 def cmp_inventory_node(a, b, invpath):
228 val_a = inventory.get_inventory_data(a["host_inventory"], invpath)
229 val_b = inventory.get_inventory_data(b["host_inventory"], invpath)
230 return cmp(val_a, val_b)
233 @painter_option_registry.register
234 class PainterOptionShowInternalTreePaths(PainterOption):
235 @property
236 def ident(self):
237 return "show_internal_tree_paths"
239 @property
240 def valuespec(self):
241 return Checkbox(
242 title=_("Show internal tree paths"),
243 default_value=False,
247 @painter_registry.register
248 class PainterInventoryTree(Painter):
249 @property
250 def ident(self):
251 return "inventory_tree"
253 @property
254 def title(self):
255 return _("Hardware & Software Tree")
257 @property
258 def columns(self):
259 return ['host_inventory', 'host_structured_status']
261 @property
262 def painter_options(self):
263 return ['show_internal_tree_paths']
265 @property
266 def load_inv(self):
267 return True
269 def render(self, row, cell):
270 return paint_host_inventory_tree(row)
274 # .--paint helper--------------------------------------------------------.
275 # | _ _ _ _ |
276 # | _ __ __ _(_)_ __ | |_ | |__ ___| |_ __ ___ _ __ |
277 # | | '_ \ / _` | | '_ \| __| | '_ \ / _ \ | '_ \ / _ \ '__| |
278 # | | |_) | (_| | | | | | |_ | | | | __/ | |_) | __/ | |
279 # | | .__/ \__,_|_|_| |_|\__| |_| |_|\___|_| .__/ \___|_| |
280 # | |_| |_| |
281 # '----------------------------------------------------------------------'
284 def decorate_inv_paint(f):
285 def wrapper(v):
286 if v in ["", None]:
287 return "", ""
288 return f(v)
290 return wrapper
293 @decorate_inv_paint
294 def inv_paint_generic(v):
295 if isinstance(v, float):
296 return "number", "%.2f" % v
297 elif isinstance(v, int):
298 return "number", "%d" % v
299 return "", html.escaper.escape_text("%s" % v)
302 @decorate_inv_paint
303 def inv_paint_hz(hz):
304 if hz < 10:
305 return "number", "%.2f" % hz
306 elif hz < 100:
307 return "number", "%.1f" % hz
308 elif hz < 1500:
309 return "number", "%.0f" % hz
310 elif hz < 1000000:
311 return "number", "%.1f kHz" % (hz / 1000)
312 elif hz < 1000000000:
313 return "number", "%.1f MHz" % (hz / 1000000)
314 return "number", "%.2f GHz" % (hz / 1000000000)
317 @decorate_inv_paint
318 def inv_paint_bytes(b):
319 if b == 0:
320 return "number", "0"
322 units = ['B', 'kB', 'MB', 'GB', 'TB']
323 i = 0
324 while b % 1024 == 0 and i + 1 < len(units):
325 b = b / 1024
326 i += 1
327 return "number", "%d %s" % (b, units[i])
330 @decorate_inv_paint
331 def inv_paint_size(b):
332 return "number", cmk.utils.render.fmt_bytes(b)
335 @decorate_inv_paint
336 def inv_paint_number(b):
337 return "number", str(b)
340 # Similar to paint_number, but is allowed to
341 # abbreviate things if numbers are very large
342 # (though it doesn't do so yet)
343 @decorate_inv_paint
344 def inv_paint_count(b):
345 return "number", str(b)
348 @decorate_inv_paint
349 def inv_paint_bytes_rounded(b):
350 if b == 0:
351 return "number", "0"
353 units = ['B', 'kB', 'MB', 'GB', 'TB']
354 i = len(units) - 1
355 fac = 1024**(len(units) - 1)
356 while b < fac * 1.5 and i > 0:
357 i -= 1
358 fac = fac / 1024.0
360 if i:
361 return "number", "%.2f&nbsp;%s" % (b / fac, units[i])
362 return "number", "%d&nbsp;%s" % (b, units[0])
365 def _nic_speed_human_readable(bits_per_second):
366 if bits_per_second == 10000000:
367 return "10 Mbit/s"
368 elif bits_per_second == 100000000:
369 return "100 Mbit/s"
370 elif bits_per_second == 1000000000:
371 return "1 Gbit/s"
372 elif bits_per_second < 1500:
373 return "%d bit/s" % bits_per_second
374 elif bits_per_second < 1000000:
375 return "%s Kbit/s" % utils.drop_dotzero(bits_per_second / 1000.0, digits=1)
376 elif bits_per_second < 1000000000:
377 return "%s Mbit/s" % utils.drop_dotzero(bits_per_second / 1000000.0, digits=2)
378 return "%s Gbit/s" % utils.drop_dotzero(bits_per_second / 1000000000.0, digits=2)
381 @decorate_inv_paint
382 def inv_paint_nic_speed(bits_per_second):
383 return "number", _nic_speed_human_readable(int(bits_per_second))
386 @decorate_inv_paint
387 def inv_paint_if_oper_status(oper_status):
388 if oper_status == 1:
389 css_class = "if_state_up"
390 elif oper_status == 2:
391 css_class = "if_state_down"
392 else:
393 css_class = "if_state_other"
395 return "if_state " + css_class, \
396 defines.interface_oper_state_name(oper_status, "%s" % oper_status).replace(" ", "&nbsp;")
399 # admin status can only be 1 or 2, matches oper status :-)
400 @decorate_inv_paint
401 def inv_paint_if_admin_status(admin_status):
402 return inv_paint_if_oper_status(admin_status)
405 @decorate_inv_paint
406 def inv_paint_if_port_type(port_type):
407 type_name = defines.interface_port_types().get(port_type, _("unknown"))
408 return "", "%d - %s" % (port_type, type_name)
411 @decorate_inv_paint
412 def inv_paint_if_available(available):
413 return "if_state " + (available and "if_available" or "if_not_available"), \
414 (available and _("free") or _("used"))
417 @decorate_inv_paint
418 def inv_paint_mssql_is_clustered(clustered):
419 return "mssql_" + (clustered and "is_clustered" or "is_not_clustered"), \
420 (clustered and _("is clustered") or _("is not clustered"))
423 @decorate_inv_paint
424 def inv_paint_mssql_node_names(node_names):
425 return "", ", ".join(node_names)
428 @decorate_inv_paint
429 def inv_paint_ipv4_network(nw):
430 if nw == "0.0.0.0/0":
431 return "", _("Default")
432 return "", nw
435 @decorate_inv_paint
436 def inv_paint_ip_address_type(t):
437 if t == "ipv4":
438 return "", _("IPv4")
439 elif t == "ipv6":
440 return "", _("IPv6")
441 return "", t
444 @decorate_inv_paint
445 def inv_paint_route_type(rt):
446 if rt == "local":
447 return "", _("Local route")
448 return "", _("Gateway route")
451 @decorate_inv_paint
452 def inv_paint_volt(volt):
453 return "number", "%.1f V" % volt
456 @decorate_inv_paint
457 def inv_paint_date(timestamp):
458 date_painted = time.strftime("%Y-%m-%d", time.localtime(timestamp))
459 return "number", "%s" % date_painted
462 @decorate_inv_paint
463 def inv_paint_date_and_time(timestamp):
464 date_painted = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
465 return "number", "%s" % date_painted
468 @decorate_inv_paint
469 def inv_paint_age(age):
470 return "number", cmk.utils.render.approx_age(age)
473 @decorate_inv_paint
474 def inv_paint_bool(value):
475 return "", (_("Yes") if value else _("No"))
478 @decorate_inv_paint
479 def inv_paint_timestamp_as_age(timestamp):
480 age = time.time() - timestamp
481 return inv_paint_age(age)
484 @decorate_inv_paint
485 def inv_paint_timestamp_as_age_days(timestamp):
486 def round_to_day(ts):
487 broken = time.localtime(ts)
488 return int(
489 time.mktime((
490 broken.tm_year,
491 broken.tm_mon,
492 broken.tm_mday,
496 broken.tm_wday,
497 broken.tm_yday,
498 broken.tm_isdst,
501 now_day = round_to_day(time.time())
502 change_day = round_to_day(timestamp)
503 age_days = (now_day - change_day) / 86400
505 css_class = "number"
506 if age_days == 0:
507 return css_class, _("today")
508 elif age_days == 1:
509 return css_class, _("yesterday")
510 return css_class, "%d %s ago" % (int(age_days), _("days"))
513 @decorate_inv_paint
514 def inv_paint_csv_labels(csv_list):
515 return "labels", html.render_br().join(csv_list.split(","))
518 @decorate_inv_paint
519 def inv_paint_cmk_label(label):
520 return "labels", render_labels({label[0]: label[1]},
521 object_type="host",
522 with_links=True,
523 label_sources={label[0]: "discovered"})
526 @decorate_inv_paint
527 def inv_paint_container_ready(ready):
528 if ready == 'yes':
529 css_class = "if_state_up"
530 elif ready == 'no':
531 css_class = "if_state_down"
532 else:
533 css_class = "if_state_other"
535 return "if_state " + css_class, ready
539 # .--display hints-------------------------------------------------------.
540 # | _ _ _ _ _ _ |
541 # | __| (_)___ _ __ | | __ _ _ _ | |__ (_)_ __ | |_ ___ |
542 # | / _` | / __| '_ \| |/ _` | | | | | '_ \| | '_ \| __/ __| |
543 # | | (_| | \__ \ |_) | | (_| | |_| | | | | | | | | | |_\__ \ |
544 # | \__,_|_|___/ .__/|_|\__,_|\__, | |_| |_|_|_| |_|\__|___/ |
545 # | |_| |___/ |
546 # '----------------------------------------------------------------------'
549 def _inv_display_hint(invpath):
550 """Generic access function to display hints
551 Don't use other methods to access the hints!"""
552 hint_id = _find_display_hint_id(invpath)
553 hint = inventory_displayhints.get(hint_id, {})
554 return _convert_display_hint(hint)
557 def _find_display_hint_id(invpath):
558 """Looks up the display hint for the given inventory path.
560 It returns either the ID of the display hint matching the given invpath
561 or None in case no entry was found.
563 In case no exact match is possible try to match display hints that use
564 some kind of *-syntax. There are two types of cases here:
566 :* -> Entries in lists (* resolves to list index numbers)
567 .* -> Path entries (* resolves to a path element)
569 The current logic has some limitations related to the ways stars can
570 be used.
572 invpath = invpath.rstrip(".:")
574 # Convert index of lists to *-syntax
575 # e.g. ".foo.bar:18.test" to ".foo.bar:*.test"
576 r = regex(r"([^\.]):[0-9]+")
577 invpath = r.sub("\\1:*", invpath)
579 candidates = [
580 invpath,
583 # Produce a list of invpath candidates with a "*" going from back to front.
585 # This algorithm only allows one ".*" in a invpath. It finds the match with
586 # the longest path prefix before the ".*".
588 # TODO: Implement a generic mechanism that allows as many stars as possible
589 invpath_parts = invpath.split(".")
590 star_index = len(invpath_parts) - 1
591 while star_index >= 0:
592 parts = invpath_parts[:star_index] + ["*"] + invpath_parts[star_index + 1:]
593 invpath_with_star = "%s" % ".".join(parts)
594 candidates.append(invpath_with_star)
595 star_index -= 1
597 for candidate in candidates:
598 # TODO: Better cleanup trailing ":" and "." from display hints at all. They are useless
599 # for finding the right entry.
600 if candidate in inventory_displayhints:
601 return candidate
603 if candidate + "." in inventory_displayhints:
604 return candidate + "."
606 if candidate + ":" in inventory_displayhints:
607 return candidate + ":"
609 return None
612 def _convert_display_hint(hint):
613 """Convert paint type to paint function, for the convenciance of the called"""
614 if "paint" in hint:
615 paint_function_name = "inv_paint_" + hint["paint"]
616 hint["paint_function"] = globals()[paint_function_name]
618 return hint
621 def inv_titleinfo(invpath, node):
622 hint = _inv_display_hint(invpath)
623 icon = hint.get("icon")
624 if "title" in hint:
625 title = hint["title"]
626 if callable(title):
627 title = title(node)
628 else:
629 title = invpath.rstrip(".").rstrip(':').split('.')[-1].split(':')[-1].replace("_",
630 " ").title()
631 return icon, title
634 # The titles of the last two path components of the node, e.g. "BIOS / Vendor"
635 def inv_titleinfo_long(invpath, node):
636 _icon, last_title = inv_titleinfo(invpath, node)
637 parent = inventory.parent_path(invpath)
638 if parent:
639 _icon, parent_title = inv_titleinfo(parent, None)
640 return parent_title + u" ➤ " + last_title
641 return last_title
644 def declare_inventory_columns():
645 # create painters for node with a display hint
646 for invpath, hint in inventory_displayhints.items():
647 if "*" not in invpath:
648 datatype = hint.get("paint", "str")
649 long_title = inv_titleinfo_long(invpath, None)
650 declare_inv_column(invpath, datatype, long_title, hint.get("short", hint["title"]))
654 # .--Datasources---------------------------------------------------------.
655 # | ____ _ |
656 # | | _ \ __ _| |_ __ _ ___ ___ _ _ _ __ ___ ___ ___ |
657 # | | | | |/ _` | __/ _` / __|/ _ \| | | | '__/ __/ _ \/ __| |
658 # | | |_| | (_| | || (_| \__ \ (_) | |_| | | | (_| __/\__ \ |
659 # | |____/ \__,_|\__\__,_|___/\___/ \__,_|_| \___\___||___/ |
660 # | |
661 # +----------------------------------------------------------------------+
662 # | Basic functions for creating datasources for for table-like infor- |
663 # | mation like software packages or network interfaces. That way the |
664 # | user can access inventory data just like normal Livestatus tables. |
665 # | This is needed for inventory data that is organized in tables. |
666 # | Data where there is one fixed path per host for an item (like the |
667 # | number of CPU cores) no datasource is being needed. These are just |
668 # | painters that are available in the hosts info. |
669 # '----------------------------------------------------------------------'
672 def _create_inv_rows(hostrow, invpath, infoname):
673 merged_tree = inventory.load_filtered_and_merged_tree(hostrow)
674 if merged_tree is None:
675 return []
676 invdata = inventory.get_inventory_data(merged_tree, invpath)
677 if invdata is None:
678 return []
679 entries = []
680 for entry in invdata:
681 newrow = {}
682 for key, value in entry.items():
683 newrow[infoname + "_" + key] = value
684 entries.append(newrow)
685 return entries
688 def inv_multisite_table(infoname, invpath, columns, add_headers, only_sites, limit, filters):
689 # Create livestatus filter for filtering out hosts
690 filter_code = ""
691 for filt in filters:
692 header = filt.filter(infoname)
693 filter_code += header
695 host_columns = ["host_name"] + list(
696 {c for c in columns if c.startswith("host_") and c != "host_name"})
697 if infoname != "invhist":
698 host_columns.append("host_structured_status")
700 query = "GET hosts\n"
701 query += "Columns: " + (" ".join(host_columns)) + "\n"
702 query += filter_code
704 if config.debug_livestatus_queries \
705 and html.output_format == "html" and display_options.enabled(display_options.W):
706 html.open_div(class_="livestatus message", onmouseover="this.style.display=\'none\';")
707 html.open_tt()
708 html.write(query.replace('\n', '<br>\n'))
709 html.close_tt()
710 html.close_div()
712 sites.live().set_only_sites(only_sites)
713 sites.live().set_prepend_site(True)
714 data = sites.live().query(query)
715 sites.live().set_prepend_site(False)
716 sites.live().set_only_sites(None)
718 headers = ["site"] + host_columns
719 # Now create big table of all inventory entries of these hosts
720 rows = []
721 for row in data:
722 hostname = row[1]
723 hostrow = dict(zip(headers, row))
724 if infoname == "invhist":
725 subrows = _create_hist_rows(hostname, columns)
726 else:
727 subrows = _create_inv_rows(hostrow, invpath, infoname)
729 for subrow in subrows:
730 subrow.update(hostrow)
731 rows.append(subrow)
732 return rows
735 def inv_find_subtable_columns(invpath):
736 """Find the name of all columns of an embedded table that have a display
737 hint. Respects the order of the columns if one is specified in the
738 display hint.
740 Also use the names found in keyorder to get even more of the available columns."""
741 subtable_hint = inventory_displayhints[invpath]
743 # Create dict from column name to its order number in the list
744 with_numbers = enumerate(subtable_hint.get("keyorder", []))
745 swapped = [(t[1], t[0]) for t in with_numbers]
746 order = dict(swapped)
748 columns = []
749 for path in inventory_displayhints.iterkeys():
750 if path.startswith(invpath + "*."):
751 # ".networking.interfaces:*.port_type" -> "port_type"
752 columns.append(path.split(".")[-1])
754 for key in subtable_hint.get("keyorder", []):
755 if key not in columns:
756 columns.append(key)
758 columns.sort(cmp=lambda a, b: cmp(order.get(a, 999), order.get(b, 999)) or cmp(a, b))
759 return columns
762 def declare_invtable_columns(infoname, invpath, topic):
763 for name in inv_find_subtable_columns(invpath):
764 sub_invpath = invpath + "*." + name
765 hint = inventory_displayhints.get(sub_invpath, {})
767 sortfunc = hint.get("sort", cmp)
768 if "paint" in hint:
769 paint_name = hint["paint"]
770 paint_function = globals()["inv_paint_" + paint_name]
771 else:
772 paint_name = "str"
773 paint_function = inv_paint_generic
775 title = inv_titleinfo(sub_invpath, None)[1]
777 # Sync this with declare_inv_column()
778 parent_class = hint.get("filter")
779 if not parent_class:
780 if paint_name == "str":
781 parent_class = FilterInvtableText
782 else:
783 parent_class = FilterInvtableIDRange
785 filter_class = type(
786 "FilterInv%s" % name.title(), (parent_class,), {
787 "_inv_info": infoname,
788 "_ident": infoname + "_" + name,
789 "_title": topic + ": " + title,
790 "_invinfo": property(lambda s: s._inv_info),
791 "sort_index": property(lambda s: 800),
792 "ident": property(lambda s: s._ident),
793 "title": property(lambda s: s._title),
796 declare_invtable_column(infoname, name, topic, title, hint.get("short", title), sortfunc,
797 paint_function, filter_class)
800 def declare_invtable_column(infoname, name, topic, title, short_title, sortfunc, paint_function,
801 filter_class):
802 column = infoname + "_" + name
803 register_painter(
804 column, {
805 "title": topic + ": " + title,
806 "short": short_title,
807 "columns": [column],
808 "paint": lambda row: paint_function(row.get(column)),
809 "sorter": column,
811 register_sorter(
812 column, {
813 "title": _("Inventory") + ": " + title,
814 "columns": [column],
815 "cmp": lambda a, b: sortfunc(a.get(column), b.get(column)),
818 filter_registry.register(filter_class)
821 class RowTableInventory(RowTable):
822 def __init__(self, info_name, inventory_path):
823 self._info_name = info_name
824 self._inventory_path = inventory_path
826 def query(self, view, columns, query, only_sites, limit, all_active_filters):
827 return inv_multisite_table(self._info_name, self._inventory_path, columns, query,
828 only_sites, limit, all_active_filters)
831 # One master function that does all
832 def declare_invtable_view(infoname, invpath, title_singular, title_plural):
833 # Declare the "info" (like a database table)
834 info_class = type(
835 "VisualInfo%s" % infoname.title(), (VisualInfo,), {
836 "_ident": infoname,
837 "ident": property(lambda self: self._ident),
838 "_title": title_singular,
839 "title": property(lambda self: self._title),
840 "_title_plural": title_plural,
841 "title_plural": property(lambda self: self._title_plural),
842 "single_spec": property(lambda self: None),
844 visual_info_registry.register(info_class)
846 # Create the datasource (like a database view)
847 ds_class = type(
848 "DataSourceInventory%s" % infoname.title(), (DataSource,), {
849 "_ident": infoname,
850 "_inventory_path": invpath,
851 "_title": "%s: %s" % (_("Inventory"), title_plural),
852 "_infos": ["host", infoname],
853 "ident": property(lambda s: s._ident),
854 "title": property(lambda s: s._title),
855 "table": property(lambda s: RowTableInventory(s._ident, s._inventory_path)),
856 "infos": property(lambda s: s._infos),
857 "keys": property(lambda s: []),
858 "id_keys": property(lambda s: []),
860 data_source_registry.register(ds_class)
862 # Declare a painter, sorter and filters for each path with display hint
863 declare_invtable_columns(infoname, invpath, title_singular)
865 # Create a nice search-view containing these columns
866 painters = []
867 filters = []
868 for name in inv_find_subtable_columns(invpath):
869 column = infoname + "_" + name
870 painters.append((column, '', ''))
871 filters.append(column)
873 # Declare two views: one for searching globally. And one
874 # for the items of one host.
876 view_spec = {
877 'datasource': infoname,
878 'topic': _('Inventory'),
879 'public': True,
880 'layout': 'table',
881 'num_columns': 1,
882 'browser_reload': 0,
883 'column_headers': 'pergroup',
884 'user_sortable': True,
885 'play_sounds': False,
886 'force_checkboxes': False,
887 'mobile': False,
888 'group_painters': [],
889 'sorters': [],
892 # View for searching for items
893 multisite_builtin_views[infoname + "_search"] = {
894 # General options
895 'title': _("Search %s") % title_plural,
896 'description': _('A view for searching in the inventory data for %s') % title_plural,
897 'hidden': False,
898 'mustsearch': True,
900 # Columns
901 'painters': [('host', 'inv_host', '')] + painters,
903 # Filters
904 'show_filters': [
905 'siteopt',
906 'hostregex',
907 'hostgroups',
908 'opthostgroup',
909 'opthost_contactgroup',
910 'host_address',
911 'host_tags',
912 'hostalias',
913 'host_favorites',
914 ] + filters,
915 'hide_filters': [],
916 'hard_filters': [],
917 'hard_filtervars': [],
919 multisite_builtin_views[infoname + "_search"].update(view_spec)
921 # View for the items of one host
922 multisite_builtin_views[infoname + "_of_host"] = {
923 # General options
924 'title': title_plural,
925 'description': _('A view for the %s of one host') % title_plural,
926 'hidden': True,
927 'mustsearch': False,
929 # Columns
930 'painters': painters,
932 # Filters
933 'show_filters': filters,
934 'hard_filters': [],
935 'hard_filtervars': [],
936 'hide_filters': ["host"],
938 multisite_builtin_views[infoname + "_of_host"].update(view_spec)
940 # View enabled checker for the _of_host view
941 view_is_enabled[infoname + "_of_host"] = _create_view_enabled_check_func(invpath)
944 def _create_view_enabled_check_func(invpath, is_history=False):
945 def _check_view_enabled(linking_view, view, context_vars):
946 context = dict(context_vars)
947 hostname = context.get("host")
948 if hostname is None:
949 return True # No host data? Keep old behaviour
950 elif hostname == "":
951 return False
953 # TODO: host is not correctly validated by visuals. Do it here for the moment.
954 try:
955 Hostname().validate_value(hostname, None)
956 except MKUserError:
957 return False
959 # FIXME In order to decide whether this view is enabled
960 # do we really need to load the whole tree?
961 struct_tree = _get_struct_tree(is_history, hostname, context.get("site"))
963 if not struct_tree:
964 return False
966 if struct_tree.is_empty():
967 return False
969 parsed_path, unused_key = inventory.parse_tree_path(invpath)
970 if parsed_path:
971 children = struct_tree.get_sub_children(parsed_path)
972 else:
973 children = [struct_tree.get_root_container()]
974 if children is None:
975 return False
976 return True
978 return _check_view_enabled
981 def _get_struct_tree(is_history, hostname, site_id):
982 struct_tree_cache = current_app.g.setdefault("struct_tree_cache", {})
983 cache_id = (is_history, hostname, site_id)
984 if cache_id in struct_tree_cache:
985 return struct_tree_cache[cache_id]
987 if is_history:
988 struct_tree = inventory.load_filtered_inventory_tree(hostname)
989 else:
990 row = inventory.get_status_data_via_livestatus(site_id, hostname)
991 struct_tree = inventory.load_filtered_and_merged_tree(row)
993 struct_tree_cache[cache_id] = struct_tree
994 return struct_tree
997 # Now declare Multisite views for a couple of embedded tables
998 declare_invtable_view(
999 "invswpac",
1000 ".software.packages:",
1001 _("Software package"),
1002 _("Software packages"),
1004 declare_invtable_view(
1005 "invinterface",
1006 ".networking.interfaces:",
1007 _("Network interface"),
1008 _("Network interfaces"),
1011 declare_invtable_view(
1012 "invdockerimages",
1013 ".software.applications.docker.images:",
1014 _("Docker images"),
1015 _("Docker images"),
1017 declare_invtable_view(
1018 "invdockercontainers",
1019 ".software.applications.docker.containers:",
1020 _("Docker containers"),
1021 _("Docker containers"),
1024 declare_invtable_view(
1025 "invother",
1026 ".hardware.components.others:",
1027 _("Other entity"),
1028 _("Other entities"),
1030 declare_invtable_view(
1031 "invunknown",
1032 ".hardware.components.unknowns:",
1033 _("Unknown entity"),
1034 _("Unknown entities"),
1036 declare_invtable_view(
1037 "invchassis",
1038 ".hardware.components.chassis:",
1039 _("Chassis"),
1040 _("Chassis"),
1042 declare_invtable_view(
1043 "invbackplane",
1044 ".hardware.components.backplanes:",
1045 _("Backplane"),
1046 _("Backplanes"),
1048 declare_invtable_view(
1049 "invcontainer",
1050 ".hardware.components.containers:",
1051 _("HW container"),
1052 _("HW containers"),
1054 declare_invtable_view(
1055 "invpsu",
1056 ".hardware.components.psus:",
1057 _("Power supply"),
1058 _("Power supplies"),
1060 declare_invtable_view(
1061 "invfan",
1062 ".hardware.components.fans:",
1063 _("Fan"),
1064 _("Fans"),
1066 declare_invtable_view(
1067 "invsensor",
1068 ".hardware.components.sensors:",
1069 _("Sensor"),
1070 _("Sensors"),
1072 declare_invtable_view(
1073 "invmodule",
1074 ".hardware.components.modules:",
1075 _("Module"),
1076 _("Modules"),
1078 declare_invtable_view(
1079 "invstack",
1080 ".hardware.components.stacks:",
1081 _("Stack"),
1082 _("Stacks"),
1085 declare_invtable_view(
1086 "invorainstance",
1087 ".software.applications.oracle.instance:",
1088 _("Oracle instance"),
1089 _("Oracle instances"),
1091 declare_invtable_view(
1092 "invorarecoveryarea",
1093 ".software.applications.oracle.recovery_area:",
1094 _("Oracle recovery area"),
1095 _("Oracle recovery areas"),
1097 declare_invtable_view(
1098 "invoradataguardstats",
1099 ".software.applications.oracle.dataguard_stats:",
1100 _("Oracle dataguard statistic"),
1101 _("Oracle dataguard statistics"),
1103 declare_invtable_view(
1104 "invoratablespace",
1105 ".software.applications.oracle.tablespaces:",
1106 _("Oracle tablespace"),
1107 _("Oracle tablespaces"),
1109 declare_invtable_view(
1110 "invorasga",
1111 ".software.applications.oracle.sga:",
1112 _("Oracle performance"),
1113 _("Oracle performance"),
1115 declare_invtable_view("invtunnels", ".networking.tunnels:", _("Networking Tunnels"),
1116 _("Networking Tunnels"))
1118 # This would also be possible. But we muss a couple of display and filter hints.
1119 # declare_invtable_view("invdisks", ".hardware.storage.disks:", _("Hard Disk"), _("Hard Disks"))
1122 # .--Views---------------------------------------------------------------.
1123 # | __ ___ |
1124 # | \ \ / (_) _____ _____ |
1125 # | \ \ / /| |/ _ \ \ /\ / / __| |
1126 # | \ V / | | __/\ V V /\__ \ |
1127 # | \_/ |_|\___| \_/\_/ |___/ |
1128 # | |
1129 # +----------------------------------------------------------------------+
1130 # | Special Multisite table views for software, ports, etc. |
1131 # '----------------------------------------------------------------------'
1133 # View for Inventory tree of one host
1134 multisite_builtin_views["inv_host"] = {
1135 # General options
1136 'datasource': 'hosts',
1137 'topic': _('Inventory'),
1138 'title': _('Inventory of host'),
1139 'linktitle': _('Inventory'),
1140 'description': _('The complete hardware- and software inventory of a host'),
1141 'icon': 'inv',
1142 'hidebutton': False,
1143 'public': True,
1144 'hidden': True,
1146 # Layout options
1147 'layout': 'dataset',
1148 'num_columns': 1,
1149 'browser_reload': 0,
1150 'column_headers': 'pergroup',
1151 'user_sortable': False,
1152 'play_sounds': False,
1153 'force_checkboxes': False,
1154 'mustsearch': False,
1155 'mobile': False,
1157 # Columns
1158 'group_painters': [],
1159 'painters': [
1160 ('host', 'host', ''),
1161 ('inv', None, ''),
1164 # Filters
1165 'hard_filters': [],
1166 'hard_filtervars': [],
1167 'hide_filters': ['host', 'site'],
1168 'show_filters': [],
1169 'sorters': [],
1172 view_is_enabled["inv_host"] = _create_view_enabled_check_func(".")
1174 generic_host_filters = multisite_builtin_views["allhosts"]["show_filters"]
1176 # View with table of all hosts, with some basic information
1177 multisite_builtin_views["inv_hosts_cpu"] = {
1178 # General options
1179 'datasource': 'hosts',
1180 'topic': _('Inventory'),
1181 'title': _('CPU Related Inventory of all Hosts'),
1182 'linktitle': _('CPU Inv. (all Hosts)'),
1183 'description': _('A list of all hosts with some CPU related inventory data'),
1184 'public': True,
1185 'hidden': False,
1187 # Layout options
1188 'layout': 'table',
1189 'num_columns': 1,
1190 'browser_reload': 0,
1191 'column_headers': 'pergroup',
1192 'user_sortable': True,
1193 'play_sounds': False,
1194 'force_checkboxes': False,
1195 'mustsearch': False,
1196 'mobile': False,
1198 # Columns
1199 'group_painters': [],
1200 'painters': [
1201 ('host', 'inv_host', ''),
1202 ('inv_software_os_name', None, ''),
1203 ('inv_hardware_cpu_cpus', None, ''),
1204 ('inv_hardware_cpu_cores', None, ''),
1205 ('inv_hardware_cpu_max_speed', None, ''),
1206 ('perfometer', None, '', 'CPU load'),
1207 ('perfometer', None, '', 'CPU utilization'),
1210 # Filters
1211 'hard_filters': ['has_inv'],
1212 'hard_filtervars': [('is_has_inv', '1')],
1213 'hide_filters': [],
1214 'show_filters': [
1215 'inv_hardware_cpu_cpus',
1216 'inv_hardware_cpu_cores',
1217 'inv_hardware_cpu_max_speed',
1219 'sorters': [],
1222 # View with available and used ethernet ports
1223 multisite_builtin_views["inv_hosts_ports"] = {
1224 # General options
1225 'datasource': 'hosts',
1226 'topic': _('Inventory'),
1227 'title': _('Switch port statistics'),
1228 'linktitle': _('Switch ports (all Hosts)'),
1229 'description':
1230 _('A list of all hosts with statistics about total, used and free networking interfaces'),
1231 'public': True,
1232 'hidden': False,
1234 # Layout options
1235 'layout': 'table',
1236 'num_columns': 1,
1237 'browser_reload': 0,
1238 'column_headers': 'pergroup',
1239 'user_sortable': True,
1240 'play_sounds': False,
1241 'force_checkboxes': False,
1242 'mustsearch': False,
1243 'mobile': False,
1245 # Columns
1246 'group_painters': [],
1247 'painters': [
1248 ('host', 'invinterface_of_host', ''),
1249 ('inv_hardware_system_product', None, ''),
1250 ('inv_networking_total_interfaces', None, ''),
1251 ('inv_networking_total_ethernet_ports', None, ''),
1252 ('inv_networking_available_ethernet_ports', None, ''),
1255 # Filters
1256 'hard_filters': ['has_inv'],
1257 'hard_filtervars': [('is_has_inv', '1')],
1258 'hide_filters': [],
1259 'show_filters': generic_host_filters + [],
1260 'sorters': [('inv_networking_available_ethernet_ports', True)],
1264 # .--History-------------------------------------------------------------.
1265 # | _ _ _ _ |
1266 # | | | | (_)___| |_ ___ _ __ _ _ |
1267 # | | |_| | / __| __/ _ \| '__| | | | |
1268 # | | _ | \__ \ || (_) | | | |_| | |
1269 # | |_| |_|_|___/\__\___/|_| \__, | |
1270 # | |___/ |
1271 # +----------------------------------------------------------------------+
1272 # | Code for history view of inventory |
1273 # '----------------------------------------------------------------------'
1276 class RowTableInventoryHistory(RowTable):
1277 def query(self, view, columns, query, only_sites, limit, all_active_filters):
1278 return inv_multisite_table("invhist", None, columns, query, only_sites, limit,
1279 all_active_filters)
1282 def _create_hist_rows(hostname, columns):
1283 for timestamp, delta_info in inventory.get_history_deltas(hostname):
1284 new, changed, removed, delta_tree = delta_info
1285 newrow = {
1286 "invhist_time": int(timestamp),
1287 "invhist_delta": delta_tree,
1288 "invhist_removed": removed,
1289 "invhist_new": new,
1290 "invhist_changed": changed,
1292 yield newrow
1295 @data_source_registry.register
1296 class DataSourceInventoryHistory(DataSource):
1297 @property
1298 def ident(self):
1299 return "invhist"
1301 @property
1302 def title(self):
1303 return _("Inventory: History")
1305 @property
1306 def table(self):
1307 return RowTableInventoryHistory()
1309 @property
1310 def infos(self):
1311 return ["host", "invhist"]
1313 @property
1314 def keys(self):
1315 return []
1317 @property
1318 def id_keys(self):
1319 return ["host_name", "invhist_time"]
1322 @painter_registry.register
1323 class PainterInvhistTime(Painter):
1324 @property
1325 def ident(self):
1326 return "invhist_time"
1328 @property
1329 def title(self):
1330 return _("Inventory Date/Time")
1332 @property
1333 def short_title(self):
1334 return _("Date/Time")
1336 @property
1337 def columns(self):
1338 return ['invhist_time']
1340 @property
1341 def painter_options(self):
1342 return ['ts_format', 'ts_date']
1344 def render(self, row, cell):
1345 return paint_age(row["invhist_time"], True, 60 * 10)
1348 @painter_registry.register
1349 class PainterInvhistDelta(Painter):
1350 @property
1351 def ident(self):
1352 return "invhist_delta"
1354 @property
1355 def title(self):
1356 return _("Inventory changes")
1358 @property
1359 def columns(self):
1360 return ['invhist_deltainvhist_time']
1362 def render(self, row, cell):
1363 return paint_host_inventory_tree(row, column="invhist_delta")
1366 def paint_invhist_count(row, what):
1367 number = row["invhist_" + what]
1368 if number:
1369 return "narrow number", str(number)
1370 return "narrow number unused", "0"
1373 @painter_registry.register
1374 class PainterInvhistRemoved(Painter):
1375 @property
1376 def ident(self):
1377 return "invhist_removed"
1379 @property
1380 def title(self):
1381 return _("Removed entries")
1383 @property
1384 def short_title(self):
1385 return _("Removed")
1387 @property
1388 def columns(self):
1389 return ['invhist_removed']
1391 def render(self, row, cell):
1392 return paint_invhist_count(row, "removed")
1395 @painter_registry.register
1396 class PainterInvhistNew(Painter):
1397 @property
1398 def ident(self):
1399 return "invhist_new"
1401 @property
1402 def title(self):
1403 return _("new entries")
1405 @property
1406 def short_title(self):
1407 return _("new")
1409 @property
1410 def columns(self):
1411 return ['invhist_new']
1413 def render(self, row, cell):
1414 return paint_invhist_count(row, "new")
1417 @painter_registry.register
1418 class PainterInvhistChanged(Painter):
1419 @property
1420 def ident(self):
1421 return "invhist_changed"
1423 @property
1424 def title(self):
1425 return _("changed entries")
1427 @property
1428 def short_title(self):
1429 return _("changed")
1431 @property
1432 def columns(self):
1433 return ['invhist_changed']
1435 def render(self, row, cell):
1436 return paint_invhist_count(row, "changed")
1439 # sorters
1440 declare_1to1_sorter("invhist_time", cmp_simple_number, reverse=True)
1441 declare_1to1_sorter("invhist_removed", cmp_simple_number)
1442 declare_1to1_sorter("invhist_new", cmp_simple_number)
1443 declare_1to1_sorter("invhist_changed", cmp_simple_number)
1445 # View for inventory history of one host
1447 multisite_builtin_views["inv_host_history"] = {
1448 # General options
1449 'datasource': 'invhist',
1450 'topic': _('Inventory'),
1451 'title': _('Inventory history of host'),
1452 'linktitle': _('Inventory History'),
1453 'description': _('The history for changes in hardware- and software inventory of a host'),
1454 'icon': 'inv',
1455 'hidebutton': False,
1456 'public': True,
1457 'hidden': True,
1459 # Layout options
1460 'layout': 'table',
1461 'num_columns': 1,
1462 'browser_reload': 0,
1463 'column_headers': 'pergroup',
1464 'user_sortable': True,
1465 'play_sounds': False,
1466 'force_checkboxes': False,
1467 'mustsearch': False,
1468 'mobile': False,
1470 # Columns
1471 'group_painters': [],
1472 'painters': [
1473 ('invhist_time', None, ''),
1474 ('invhist_removed', None, ''),
1475 ('invhist_new', None, ''),
1476 ('invhist_changed', None, ''),
1477 ('invhist_delta', None, ''),
1480 # Filters
1481 'hard_filters': [],
1482 'hard_filtervars': [],
1483 'hide_filters': ['host'],
1484 'show_filters': [],
1485 'sorters': [('invhist_time', False)],
1488 view_is_enabled["inv_host_history"] = _create_view_enabled_check_func(".", is_history=True)
1491 # .--Node Renderer-------------------------------------------------------.
1492 # | _ _ _ ____ _ |
1493 # | | \ | | ___ __| | ___ | _ \ ___ _ __ __| | ___ _ __ ___ _ __ |
1494 # | | \| |/ _ \ / _` |/ _ \ | |_) / _ \ '_ \ / _` |/ _ \ '__/ _ \ '__| |
1495 # | | |\ | (_) | (_| | __/ | _ < __/ | | | (_| | __/ | | __/ | |
1496 # | |_| \_|\___/ \__,_|\___| |_| \_\___|_| |_|\__,_|\___|_| \___|_| |
1497 # | |
1498 # '----------------------------------------------------------------------'
1501 # Just for compatibility
1502 def render_inv_dicttable(*args):
1503 pass
1506 class NodeRenderer(object):
1507 def __init__(self, site_id, hostname, tree_id, invpath, show_internal_tree_paths=False):
1508 self._site_id = site_id
1509 self._hostname = hostname
1510 self._tree_id = tree_id
1511 self._invpath = invpath
1512 if show_internal_tree_paths:
1513 self._show_internal_tree_paths = "on"
1514 else:
1515 self._show_internal_tree_paths = ""
1517 # ---container------------------------------------------------------------
1519 def show_container(self, container, path=None):
1520 for _, node in container.get_edge_nodes():
1521 node_abs_path = node.get_absolute_path()
1523 raw_invpath = self._get_raw_path(".".join(map(str, node_abs_path)))
1524 invpath = ".%s." % raw_invpath
1526 icon, title = inv_titleinfo(invpath, node)
1528 # Replace placeholders in title with the real values for this path
1529 if "%d" in title or "%s" in title:
1530 title = self._replace_placeholders(title, invpath)
1532 header = self._get_header(title, ".".join(map(str, node_abs_path)), "#666")
1533 fetch_url = html.makeuri_contextless(
1535 ("site", self._site_id),
1536 ("host", self._hostname),
1537 ("path", invpath),
1538 ("show_internal_tree_paths", self._show_internal_tree_paths),
1539 ("treeid", self._tree_id),
1541 "ajax_inv_render_tree.py",
1544 if html.begin_foldable_container(
1545 "inv_%s%s" % (self._hostname, self._tree_id),
1546 invpath,
1547 False,
1548 header,
1549 icon=icon,
1550 fetch_url=fetch_url,
1551 tree_img="tree_black"):
1552 # Render only if it is open. We'll get the stuff via ajax later if it's closed
1553 for child in inventory.sort_children(node.get_node_children()):
1554 child.show(self, path=raw_invpath)
1555 html.end_foldable_container()
1557 def _replace_placeholders(self, raw_title, invpath):
1558 hint_id = _find_display_hint_id(invpath)
1559 invpath_parts = invpath.strip(".").split(".")
1561 # Use the position of the stars in the path to build a list of texts
1562 # that should be used for replacing the tile placeholders
1563 replace_vars = []
1564 hint_parts = hint_id.strip(".").split(".")
1565 for index, hint_part in enumerate(hint_parts):
1566 if hint_part == "*":
1567 replace_vars.append(invpath_parts[index])
1569 # Now replace the variables in the title. Handle the case where we have
1570 # more stars than macros in the title.
1571 num_macros = raw_title.count("%d") + raw_title.count("%s")
1572 return raw_title % tuple(replace_vars[:num_macros])
1574 # ---numeration-----------------------------------------------------------
1576 def show_numeration(self, numeration, path=None):
1577 #FIXME these kind of paths are required for hints.
1578 # Clean this up one day.
1579 invpath = ".%s:" % self._get_raw_path(path)
1580 hint = _inv_display_hint(invpath)
1581 keyorder = hint.get("keyorder", []) # well known keys
1582 data = numeration.get_child_data()
1584 # Add titles for those keys
1585 titles = []
1586 for key in keyorder:
1587 sub_invpath = "%s0.%s" % (invpath, key)
1588 _icon, title = inv_titleinfo(sub_invpath, None)
1589 sub_hint = _inv_display_hint(sub_invpath)
1590 short_title = sub_hint.get("short", title)
1591 titles.append((short_title, key))
1593 # Determine *all* keys, in order to find unknown ones
1594 keys = self._get_numeration_keys(data)
1596 # Order not well-known keys alphabetically
1597 extratitles = []
1598 for key in keys:
1599 if key not in keyorder:
1600 _icon, title = inv_titleinfo("%s0.%s" % (invpath, key), None)
1601 extratitles.append((title, key))
1602 extratitles.sort()
1603 titles += extratitles
1605 # Link to Multisite view with exactly this table
1606 if "view" in hint:
1607 url = html.makeuri_contextless(
1609 ("view_name", hint["view"]),
1610 ("host", self._hostname),
1612 filename="view.py",
1614 html.div(
1615 html.render_a(_("Open this table for filtering / sorting"), href=url),
1616 class_="invtablelink")
1618 self._show_numeration_table(titles, invpath, data)
1620 def _get_numeration_keys(self, data):
1621 keys = set([])
1622 for entry in data:
1623 keys.update(entry.keys())
1624 return keys
1626 def _show_numeration_table(self, titles, invpath, data):
1627 # TODO: Use table.open_table() below.
1628 html.open_table(class_="data")
1629 html.open_tr()
1630 for title, key in titles:
1631 html.th(self._get_header(title, key, "#DDD"))
1632 html.close_tr()
1633 for index, entry in enumerate(data):
1634 html.open_tr(class_="even0")
1635 for title, key in titles:
1636 value = entry.get(key)
1637 sub_invpath = "%s%d.%s" % (invpath, index, key)
1638 hint = _inv_display_hint(sub_invpath)
1639 if "paint_function" in hint:
1640 #FIXME At the moment we need it to get tdclass
1641 # Clean this up one day.
1642 # The value is not really needed, but we need to deal with the delta mode
1643 unused_value = value[1] if isinstance(value, tuple) else value
1644 tdclass, _ = hint["paint_function"](unused_value)
1645 else:
1646 tdclass = None
1648 html.open_td(class_=tdclass)
1649 self._show_numeration_value(value, hint)
1650 html.close_td()
1651 html.close_tr()
1652 html.close_table()
1654 def _show_numeration_value(self, value, hint):
1655 raise NotImplementedError()
1657 # ---attributes-----------------------------------------------------------
1659 def show_attributes(self, attributes, path=None):
1660 invpath = ".%s" % self._get_raw_path(path)
1661 hint = _inv_display_hint(invpath)
1663 def _sort_attributes(item):
1664 """Sort the attributes by the configured key order. In case no key order
1665 is given sort by the key. In case there is a key order and a key is not
1666 in the list, put it at the end and sort all of those by key."""
1667 key = item[0]
1668 keyorder = hint.get("keyorder")
1669 if not keyorder:
1670 return key
1672 try:
1673 return keyorder.index(key)
1674 except ValueError:
1675 return len(keyorder) + 1, key
1677 html.open_table()
1678 for key, value in sorted(attributes.get_child_data().iteritems(), key=_sort_attributes):
1679 sub_invpath = "%s.%s" % (invpath, key)
1680 _icon, title = inv_titleinfo(sub_invpath, key)
1681 hint = _inv_display_hint(sub_invpath)
1683 html.open_tr()
1684 html.open_th(title=sub_invpath)
1685 html.write(self._get_header(title, key, "#DDD"))
1686 html.close_th()
1687 html.open_td()
1688 self.show_attribute(value, hint)
1689 html.close_td()
1690 html.close_tr()
1691 html.close_table()
1693 def show_attribute(self, value, hint):
1694 raise NotImplementedError()
1696 # ---helper---------------------------------------------------------------
1698 def _get_raw_path(self, path):
1699 if path is None:
1700 return self._invpath.strip(".")
1701 return path.strip(".")
1703 def _get_header(self, title, key, hex_color):
1704 header = HTML(title)
1705 if self._show_internal_tree_paths:
1706 header += HTML(" <span style='color: %s'>(%s)</span>" % (hex_color, key))
1707 return header
1709 def _show_child_value(self, value, hint):
1710 if "paint_function" in hint:
1711 _tdclass, code = hint["paint_function"](value)
1712 html.write(code)
1713 elif isinstance(value, str):
1714 try:
1715 text = value.decode("utf-8")
1716 except:
1717 text = value
1718 html.write_text(text)
1719 elif isinstance(value, unicode):
1720 html.write_text(value)
1721 elif isinstance(value, int):
1722 html.write(str(value))
1723 elif isinstance(value, float):
1724 html.write("%.2f" % value)
1725 elif value is not None:
1726 html.write(str(value))
1729 class AttributeRenderer(NodeRenderer):
1730 def _show_numeration_value(self, value, hint):
1731 self._show_child_value(value, hint)
1733 def show_attribute(self, value, hint):
1734 self._show_child_value(value, hint)
1737 class DeltaNodeRenderer(NodeRenderer):
1738 def _show_numeration_value(self, value, hint):
1739 if value is None:
1740 value = (None, None)
1741 self.show_attribute(value, hint)
1743 def show_attribute(self, value, hint):
1744 old, new = value
1745 if old is None and new is not None:
1746 html.open_span(class_="invnew")
1747 self._show_child_value(new, hint)
1748 html.close_span()
1749 elif old is not None and new is None:
1750 html.open_span(class_="invold")
1751 self._show_child_value(old, hint)
1752 html.close_span()
1753 elif old == new:
1754 self._show_child_value(old, hint)
1755 elif old is not None and new is not None:
1756 html.open_span(class_="invold")
1757 self._show_child_value(old, hint)
1758 html.close_span()
1759 html.write(u" → ")
1760 html.open_span(class_="invnew")
1761 self._show_child_value(new, hint)
1762 html.close_span()
1765 # Ajax call for fetching parts of the tree
1766 @cmk.gui.pages.register("ajax_inv_render_tree")
1767 def ajax_inv_render_tree():
1768 site_id = html.request.var("site")
1769 hostname = html.request.var("host")
1770 invpath = html.request.var("path")
1771 tree_id = html.request.var("treeid", "")
1772 show_internal_tree_paths = bool(html.request.var("show_internal_tree_paths"))
1773 if tree_id:
1774 struct_tree = inventory.load_delta_tree(hostname, int(tree_id[1:]))
1775 tree_renderer = DeltaNodeRenderer(site_id, hostname, tree_id, invpath)
1776 else:
1777 row = inventory.get_status_data_via_livestatus(site_id, hostname)
1778 struct_tree = inventory.load_filtered_and_merged_tree(row)
1779 tree_renderer = AttributeRenderer(
1780 site_id, hostname, "", invpath, show_internal_tree_paths=show_internal_tree_paths)
1782 if struct_tree is None:
1783 html.show_error(_("No such inventory tree."))
1785 parsed_path, _attributes_key = inventory.parse_tree_path(invpath)
1786 if parsed_path:
1787 children = struct_tree.get_sub_children(parsed_path)
1788 else:
1789 children = [struct_tree.get_root_container()]
1791 if children is None:
1792 html.show_error(
1793 _("Invalid path in inventory tree: '%s' >> %s") % (invpath, repr(parsed_path)))
1794 else:
1795 for child in inventory.sort_children(children):
1796 child.show(tree_renderer, path=invpath)