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.
29 from cmk
.utils
.regex
import regex
30 import cmk
.utils
.defines
as defines
31 import cmk
.utils
.render
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 (
49 from cmk
.gui
.plugins
.visuals
.inventory
import (
54 FilterInvtableIDRange
,
57 from cmk
.gui
.plugins
.views
import (
66 painter_option_registry
,
69 inventory_displayhints
,
70 multisite_builtin_views
,
79 def paint_host_inventory_tree(row
, invpath
=".", column
="host_inventory"):
80 struct_tree
= row
.get(column
)
81 if struct_tree
is None:
84 if column
== "host_inventory":
85 painter_options
= PainterOptions
.get_instance()
86 tree_renderer
= AttributeRenderer(
91 show_internal_tree_paths
=painter_options
.get('show_internal_tree_paths'))
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
,
103 def _paint_host_inventory_tree_children(struct_tree
, parsed_path
, tree_renderer
):
105 children
= struct_tree
.get_sub_children(parsed_path
)
107 children
= [struct_tree
.get_root_container()]
111 for child
in children
:
112 child
.show(tree_renderer
)
114 return "invtree", code
117 def _paint_host_inventory_tree_value(struct_tree
, parsed_path
, tree_renderer
, invpath
,
119 if attributes_key
== []:
120 child
= struct_tree
.get_sub_numeration(parsed_path
)
122 child
= struct_tree
.get_sub_attributes(parsed_path
)
128 if invpath
.endswith(".") or invpath
.endswith(":"):
129 invpath
= invpath
[:-1]
130 if attributes_key
== []:
131 tree_renderer
.show_numeration(child
, path
=invpath
)
133 tree_renderer
.show_attribute(child
.get_child_data().get(attributes_key
),
134 _inv_display_hint(invpath
))
139 def _inv_filter_info():
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):
167 name
= "inv_" + invpath
.replace(":", "_").replace(".", "_").strip("_")
169 # Declare column painter
171 "title": invpath
== "." and _("Inventory Tree") or (_("Inventory") + ": " + title
),
172 "columns": ["host_inventory", "host_structured_status"],
173 "options": ["show_internal_tree_paths"],
175 "paint": lambda row
: paint_host_inventory_tree(row
, invpath
),
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
187 "title": _("Inventory") + ": " + title
,
188 "columns": ["host_inventory", "host_structured_status"],
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
199 "FilterInv%s" % name
.title(), (parent_class
,), {
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
),
210 "FilterInv%s" % name
.title(), (FilterInvFloat
,), {
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
):
237 return "show_internal_tree_paths"
242 title
=_("Show internal tree paths"),
247 @painter_registry.register
248 class PainterInventoryTree(Painter
):
251 return "inventory_tree"
255 return _("Hardware & Software Tree")
259 return ['host_inventory', 'host_structured_status']
262 def painter_options(self
):
263 return ['show_internal_tree_paths']
269 def render(self
, row
, cell
):
270 return paint_host_inventory_tree(row
)
274 # .--paint helper--------------------------------------------------------.
276 # | _ __ __ _(_)_ __ | |_ | |__ ___| |_ __ ___ _ __ |
277 # | | '_ \ / _` | | '_ \| __| | '_ \ / _ \ | '_ \ / _ \ '__| |
278 # | | |_) | (_| | | | | | |_ | | | | __/ | |_) | __/ | |
279 # | | .__/ \__,_|_|_| |_|\__| |_| |_|\___|_| .__/ \___|_| |
281 # '----------------------------------------------------------------------'
284 def decorate_inv_paint(f
):
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
)
303 def inv_paint_hz(hz
):
305 return "number", "%.2f" % hz
307 return "number", "%.1f" % hz
309 return "number", "%.0f" % hz
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)
318 def inv_paint_bytes(b
):
322 units
= ['B', 'kB', 'MB', 'GB', 'TB']
324 while b
% 1024 == 0 and i
+ 1 < len(units
):
327 return "number", "%d %s" % (b
, units
[i
])
331 def inv_paint_size(b
):
332 return "number", cmk
.utils
.render
.fmt_bytes(b
)
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)
344 def inv_paint_count(b
):
345 return "number", str(b
)
349 def inv_paint_bytes_rounded(b
):
353 units
= ['B', 'kB', 'MB', 'GB', 'TB']
355 fac
= 1024**(len(units
) - 1)
356 while b
< fac
* 1.5 and i
> 0:
361 return "number", "%.2f %s" % (b
/ fac
, units
[i
])
362 return "number", "%d %s" % (b
, units
[0])
365 def _nic_speed_human_readable(bits_per_second
):
366 if bits_per_second
== 10000000:
368 elif bits_per_second
== 100000000:
370 elif bits_per_second
== 1000000000:
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)
382 def inv_paint_nic_speed(bits_per_second
):
383 return "number", _nic_speed_human_readable(int(bits_per_second
))
387 def inv_paint_if_oper_status(oper_status
):
389 css_class
= "if_state_up"
390 elif oper_status
== 2:
391 css_class
= "if_state_down"
393 css_class
= "if_state_other"
395 return "if_state " + css_class
, \
396 defines
.interface_oper_state_name(oper_status
, "%s" % oper_status
).replace(" ", " ")
399 # admin status can only be 1 or 2, matches oper status :-)
401 def inv_paint_if_admin_status(admin_status
):
402 return inv_paint_if_oper_status(admin_status
)
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
)
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"))
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"))
424 def inv_paint_mssql_node_names(node_names
):
425 return "", ", ".join(node_names
)
429 def inv_paint_ipv4_network(nw
):
430 if nw
== "0.0.0.0/0":
431 return "", _("Default")
436 def inv_paint_ip_address_type(t
):
445 def inv_paint_route_type(rt
):
447 return "", _("Local route")
448 return "", _("Gateway route")
452 def inv_paint_volt(volt
):
453 return "number", "%.1f V" % volt
457 def inv_paint_date(timestamp
):
458 date_painted
= time
.strftime("%Y-%m-%d", time
.localtime(timestamp
))
459 return "number", "%s" % date_painted
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
469 def inv_paint_age(age
):
470 return "number", cmk
.utils
.render
.approx_age(age
)
474 def inv_paint_bool(value
):
475 return "", (_("Yes") if value
else _("No"))
479 def inv_paint_timestamp_as_age(timestamp
):
480 age
= time
.time() - timestamp
481 return inv_paint_age(age
)
485 def inv_paint_timestamp_as_age_days(timestamp
):
486 def round_to_day(ts
):
487 broken
= time
.localtime(ts
)
501 now_day
= round_to_day(time
.time())
502 change_day
= round_to_day(timestamp
)
503 age_days
= (now_day
- change_day
) / 86400
507 return css_class
, _("today")
509 return css_class
, _("yesterday")
510 return css_class
, "%d %s ago" % (int(age_days
), _("days"))
514 def inv_paint_csv_labels(csv_list
):
515 return "labels", html
.render_br().join(csv_list
.split(","))
519 def inv_paint_cmk_label(label
):
520 return "labels", render_labels({label
[0]: label
[1]},
523 label_sources
={label
[0]: "discovered"})
527 def inv_paint_container_ready(ready
):
529 css_class
= "if_state_up"
531 css_class
= "if_state_down"
533 css_class
= "if_state_other"
535 return "if_state " + css_class
, ready
539 # .--display hints-------------------------------------------------------.
541 # | __| (_)___ _ __ | | __ _ _ _ | |__ (_)_ __ | |_ ___ |
542 # | / _` | / __| '_ \| |/ _` | | | | | '_ \| | '_ \| __/ __| |
543 # | | (_| | \__ \ |_) | | (_| | |_| | | | | | | | | | |_\__ \ |
544 # | \__,_|_|___/ .__/|_|\__,_|\__, | |_| |_|_|_| |_|\__|___/ |
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
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
)
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
)
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
:
603 if candidate
+ "." in inventory_displayhints
:
604 return candidate
+ "."
606 if candidate
+ ":" in inventory_displayhints
:
607 return candidate
+ ":"
612 def _convert_display_hint(hint
):
613 """Convert paint type to paint function, for the convenciance of the called"""
615 paint_function_name
= "inv_paint_" + hint
["paint"]
616 hint
["paint_function"] = globals()[paint_function_name
]
621 def inv_titleinfo(invpath
, node
):
622 hint
= _inv_display_hint(invpath
)
623 icon
= hint
.get("icon")
625 title
= hint
["title"]
629 title
= invpath
.rstrip(".").rstrip(':').split('.')[-1].split(':')[-1].replace("_",
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
)
639 _icon
, parent_title
= inv_titleinfo(parent
, None)
640 return parent_title
+ u
" ➤ " + 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---------------------------------------------------------.
656 # | | _ \ __ _| |_ __ _ ___ ___ _ _ _ __ ___ ___ ___ |
657 # | | | | |/ _` | __/ _` / __|/ _ \| | | | '__/ __/ _ \/ __| |
658 # | | |_| | (_| | || (_| \__ \ (_) | |_| | | | (_| __/\__ \ |
659 # | |____/ \__,_|\__\__,_|___/\___/ \__,_|_| \___\___||___/ |
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:
676 invdata
= inventory
.get_inventory_data(merged_tree
, invpath
)
680 for entry
in invdata
:
682 for key
, value
in entry
.items():
683 newrow
[infoname
+ "_" + key
] = value
684 entries
.append(newrow
)
688 def inv_multisite_table(infoname
, invpath
, columns
, add_headers
, only_sites
, limit
, filters
):
689 # Create livestatus filter for filtering out hosts
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"
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\';")
708 html
.write(query
.replace('\n', '<br>\n'))
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
723 hostrow
= dict(zip(headers
, row
))
724 if infoname
== "invhist":
725 subrows
= _create_hist_rows(hostname
, columns
)
727 subrows
= _create_inv_rows(hostrow
, invpath
, infoname
)
729 for subrow
in subrows
:
730 subrow
.update(hostrow
)
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
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
)
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
:
758 columns
.sort(cmp=lambda a
, b
: cmp(order
.get(a
, 999), order
.get(b
, 999)) or cmp(a
, b
))
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)
769 paint_name
= hint
["paint"]
770 paint_function
= globals()["inv_paint_" + paint_name
]
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")
780 if paint_name
== "str":
781 parent_class
= FilterInvtableText
783 parent_class
= FilterInvtableIDRange
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
,
802 column
= infoname
+ "_" + name
805 "title": topic
+ ": " + title
,
806 "short": short_title
,
808 "paint": lambda row
: paint_function(row
.get(column
)),
813 "title": _("Inventory") + ": " + title
,
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)
835 "VisualInfo%s" % infoname
.title(), (VisualInfo
,), {
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)
848 "DataSourceInventory%s" % infoname
.title(), (DataSource
,), {
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
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.
877 'datasource': infoname
,
878 'topic': _('Inventory'),
883 'column_headers': 'pergroup',
884 'user_sortable': True,
885 'play_sounds': False,
886 'force_checkboxes': False,
888 'group_painters': [],
892 # View for searching for items
893 multisite_builtin_views
[infoname
+ "_search"] = {
895 'title': _("Search %s") % title_plural
,
896 'description': _('A view for searching in the inventory data for %s') % title_plural
,
901 'painters': [('host', 'inv_host', '')] + painters
,
909 'opthost_contactgroup',
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"] = {
924 'title': title_plural
,
925 'description': _('A view for the %s of one host') % title_plural
,
930 'painters': painters
,
933 'show_filters': 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")
949 return True # No host data? Keep old behaviour
953 # TODO: host is not correctly validated by visuals. Do it here for the moment.
955 Hostname().validate_value(hostname
, None)
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"))
966 if struct_tree
.is_empty():
969 parsed_path
, unused_key
= inventory
.parse_tree_path(invpath
)
971 children
= struct_tree
.get_sub_children(parsed_path
)
973 children
= [struct_tree
.get_root_container()]
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
]
988 struct_tree
= inventory
.load_filtered_inventory_tree(hostname
)
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
997 # Now declare Multisite views for a couple of embedded tables
998 declare_invtable_view(
1000 ".software.packages:",
1001 _("Software package"),
1002 _("Software packages"),
1004 declare_invtable_view(
1006 ".networking.interfaces:",
1007 _("Network interface"),
1008 _("Network interfaces"),
1011 declare_invtable_view(
1013 ".software.applications.docker.images:",
1017 declare_invtable_view(
1018 "invdockercontainers",
1019 ".software.applications.docker.containers:",
1020 _("Docker containers"),
1021 _("Docker containers"),
1024 declare_invtable_view(
1026 ".hardware.components.others:",
1028 _("Other entities"),
1030 declare_invtable_view(
1032 ".hardware.components.unknowns:",
1033 _("Unknown entity"),
1034 _("Unknown entities"),
1036 declare_invtable_view(
1038 ".hardware.components.chassis:",
1042 declare_invtable_view(
1044 ".hardware.components.backplanes:",
1048 declare_invtable_view(
1050 ".hardware.components.containers:",
1054 declare_invtable_view(
1056 ".hardware.components.psus:",
1058 _("Power supplies"),
1060 declare_invtable_view(
1062 ".hardware.components.fans:",
1066 declare_invtable_view(
1068 ".hardware.components.sensors:",
1072 declare_invtable_view(
1074 ".hardware.components.modules:",
1078 declare_invtable_view(
1080 ".hardware.components.stacks:",
1085 declare_invtable_view(
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(
1105 ".software.applications.oracle.tablespaces:",
1106 _("Oracle tablespace"),
1107 _("Oracle tablespaces"),
1109 declare_invtable_view(
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---------------------------------------------------------------.
1124 # | \ \ / (_) _____ _____ |
1125 # | \ \ / /| |/ _ \ \ /\ / / __| |
1126 # | \ V / | | __/\ V V /\__ \ |
1127 # | \_/ |_|\___| \_/\_/ |___/ |
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"] = {
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'),
1142 'hidebutton': False,
1147 'layout': 'dataset',
1149 'browser_reload': 0,
1150 'column_headers': 'pergroup',
1151 'user_sortable': False,
1152 'play_sounds': False,
1153 'force_checkboxes': False,
1154 'mustsearch': False,
1158 'group_painters': [],
1160 ('host', 'host', ''),
1166 'hard_filtervars': [],
1167 'hide_filters': ['host', 'site'],
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"] = {
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'),
1190 'browser_reload': 0,
1191 'column_headers': 'pergroup',
1192 'user_sortable': True,
1193 'play_sounds': False,
1194 'force_checkboxes': False,
1195 'mustsearch': False,
1199 'group_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'),
1211 'hard_filters': ['has_inv'],
1212 'hard_filtervars': [('is_has_inv', '1')],
1215 'inv_hardware_cpu_cpus',
1216 'inv_hardware_cpu_cores',
1217 'inv_hardware_cpu_max_speed',
1222 # View with available and used ethernet ports
1223 multisite_builtin_views
["inv_hosts_ports"] = {
1225 'datasource': 'hosts',
1226 'topic': _('Inventory'),
1227 'title': _('Switch port statistics'),
1228 'linktitle': _('Switch ports (all Hosts)'),
1230 _('A list of all hosts with statistics about total, used and free networking interfaces'),
1237 'browser_reload': 0,
1238 'column_headers': 'pergroup',
1239 'user_sortable': True,
1240 'play_sounds': False,
1241 'force_checkboxes': False,
1242 'mustsearch': False,
1246 'group_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, ''),
1256 'hard_filters': ['has_inv'],
1257 'hard_filtervars': [('is_has_inv', '1')],
1259 'show_filters': generic_host_filters
+ [],
1260 'sorters': [('inv_networking_available_ethernet_ports', True)],
1264 # .--History-------------------------------------------------------------.
1266 # | | | | (_)___| |_ ___ _ __ _ _ |
1267 # | | |_| | / __| __/ _ \| '__| | | | |
1268 # | | _ | \__ \ || (_) | | | |_| | |
1269 # | |_| |_|_|___/\__\___/|_| \__, | |
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
,
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
1286 "invhist_time": int(timestamp
),
1287 "invhist_delta": delta_tree
,
1288 "invhist_removed": removed
,
1290 "invhist_changed": changed
,
1295 @data_source_registry.register
1296 class DataSourceInventoryHistory(DataSource
):
1303 return _("Inventory: History")
1307 return RowTableInventoryHistory()
1311 return ["host", "invhist"]
1319 return ["host_name", "invhist_time"]
1322 @painter_registry.register
1323 class PainterInvhistTime(Painter
):
1326 return "invhist_time"
1330 return _("Inventory Date/Time")
1333 def short_title(self
):
1334 return _("Date/Time")
1338 return ['invhist_time']
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
):
1352 return "invhist_delta"
1356 return _("Inventory changes")
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
]
1369 return "narrow number", str(number
)
1370 return "narrow number unused", "0"
1373 @painter_registry.register
1374 class PainterInvhistRemoved(Painter
):
1377 return "invhist_removed"
1381 return _("Removed entries")
1384 def short_title(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
):
1399 return "invhist_new"
1403 return _("new entries")
1406 def short_title(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
):
1421 return "invhist_changed"
1425 return _("changed entries")
1428 def short_title(self
):
1433 return ['invhist_changed']
1435 def render(self
, row
, cell
):
1436 return paint_invhist_count(row
, "changed")
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"] = {
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'),
1455 'hidebutton': False,
1462 'browser_reload': 0,
1463 'column_headers': 'pergroup',
1464 'user_sortable': True,
1465 'play_sounds': False,
1466 'force_checkboxes': False,
1467 'mustsearch': False,
1471 'group_painters': [],
1473 ('invhist_time', None, ''),
1474 ('invhist_removed', None, ''),
1475 ('invhist_new', None, ''),
1476 ('invhist_changed', None, ''),
1477 ('invhist_delta', None, ''),
1482 'hard_filtervars': [],
1483 'hide_filters': ['host'],
1485 'sorters': [('invhist_time', False)],
1488 view_is_enabled
["inv_host_history"] = _create_view_enabled_check_func(".", is_history
=True)
1491 # .--Node Renderer-------------------------------------------------------.
1493 # | | \ | | ___ __| | ___ | _ \ ___ _ __ __| | ___ _ __ ___ _ __ |
1494 # | | \| |/ _ \ / _` |/ _ \ | |_) / _ \ '_ \ / _` |/ _ \ '__/ _ \ '__| |
1495 # | | |\ | (_) | (_| | __/ | _ < __/ | | | (_| | __/ | | __/ | |
1496 # | |_| \_|\___/ \__,_|\___| |_| \_\___|_| |_|\__,_|\___|_| \___|_| |
1498 # '----------------------------------------------------------------------'
1501 # Just for compatibility
1502 def render_inv_dicttable(*args
):
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"
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
),
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
),
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
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
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
1599 if key
not in keyorder
:
1600 _icon
, title
= inv_titleinfo("%s0.%s" % (invpath
, key
), None)
1601 extratitles
.append((title
, key
))
1603 titles
+= extratitles
1605 # Link to Multisite view with exactly this table
1607 url
= html
.makeuri_contextless(
1609 ("view_name", hint
["view"]),
1610 ("host", self
._hostname
),
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
):
1623 keys
.update(entry
.keys())
1626 def _show_numeration_table(self
, titles
, invpath
, data
):
1627 # TODO: Use table.open_table() below.
1628 html
.open_table(class_
="data")
1630 for title
, key
in titles
:
1631 html
.th(self
._get
_header
(title
, key
, "#DDD"))
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
)
1648 html
.open_td(class_
=tdclass
)
1649 self
._show
_numeration
_value
(value
, hint
)
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."""
1668 keyorder
= hint
.get("keyorder")
1673 return keyorder
.index(key
)
1675 return len(keyorder
) + 1, key
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
)
1684 html
.open_th(title
=sub_invpath
)
1685 html
.write(self
._get
_header
(title
, key
, "#DDD"))
1688 self
.show_attribute(value
, hint
)
1693 def show_attribute(self
, value
, hint
):
1694 raise NotImplementedError()
1696 # ---helper---------------------------------------------------------------
1698 def _get_raw_path(self
, path
):
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
))
1709 def _show_child_value(self
, value
, hint
):
1710 if "paint_function" in hint
:
1711 _tdclass
, code
= hint
["paint_function"](value
)
1713 elif isinstance(value
, str):
1715 text
= value
.decode("utf-8")
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
):
1740 value
= (None, None)
1741 self
.show_attribute(value
, hint
)
1743 def show_attribute(self
, value
, hint
):
1745 if old
is None and new
is not None:
1746 html
.open_span(class_
="invnew")
1747 self
._show
_child
_value
(new
, hint
)
1749 elif old
is not None and new
is None:
1750 html
.open_span(class_
="invold")
1751 self
._show
_child
_value
(old
, hint
)
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
)
1760 html
.open_span(class_
="invnew")
1761 self
._show
_child
_value
(new
, hint
)
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"))
1774 struct_tree
= inventory
.load_delta_tree(hostname
, int(tree_id
[1:]))
1775 tree_renderer
= DeltaNodeRenderer(site_id
, hostname
, tree_id
, invpath
)
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
)
1787 children
= struct_tree
.get_sub_children(parsed_path
)
1789 children
= [struct_tree
.get_root_container()]
1791 if children
is None:
1793 _("Invalid path in inventory tree: '%s' >> %s") % (invpath
, repr(parsed_path
)))
1795 for child
in inventory
.sort_children(children
):
1796 child
.show(tree_renderer
, path
=invpath
)