Add specific visualization for labels depending on their source
[check_mk.git] / cmk / gui / valuespec.py
blobfef98e4b53fd1c3d23a8507e342edb8bba05355d
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 # FIXME: Cleanups
28 # - Change all class to be "new style" classes
29 # - Consolidate ListChoice and DualListChoice to use the same class
30 # and rename to better name
31 # - Consolidate RadioChoice and DropdownChoice to use same class
32 # and rename to better name
33 # - Consolidate ListOf and ListOfStrings/ListOfIntegers
34 # - Checkbox
35 # -> rename to Boolean
36 # -> Add alternative rendering "dropdown"
38 import base64
39 from enum import Enum
40 import hashlib
41 import ipaddress
42 import json
43 import math
44 import numbers
45 import os
46 import re
47 import socket
48 import sre_constants
49 import time
50 import types
51 import urlparse
52 from UserDict import DictMixin
53 from StringIO import StringIO
54 from PIL import Image
56 from Cryptodome.PublicKey import RSA
57 import six
59 import cmk.utils.log
60 import cmk.utils.paths
61 import cmk.utils.defines as defines
63 import cmk.gui.forms as forms
64 import cmk.gui.utils as utils
65 from cmk.gui.i18n import _
66 from cmk.gui.pages import page_registry, Page, AjaxPage
67 from cmk.gui.globals import html
68 from cmk.gui.htmllib import HTML
69 from cmk.gui.exceptions import MKUserError, MKGeneralException
71 import livestatus
74 def _type_name(v):
75 try:
76 return type(v).__name__
77 except:
78 return html.attrencode(type(v))
81 seconds_per_day = 86400
84 # Abstract base class of all value declaration classes.
85 class ValueSpec(object):
86 def __init__(self, **kwargs):
87 super(ValueSpec, self).__init__()
88 self._title = kwargs.get("title")
89 self._help = kwargs.get("help")
90 if "default_value" in kwargs:
91 self._default_value = kwargs.get("default_value")
92 self._validate = kwargs.get("validate")
94 # debug display allows to view the class name of a certain element in the gui.
95 # If set False, then the corresponding div is present, but hidden.
96 self.debug_display = False
98 def title(self):
99 return self._title
101 def help(self):
102 if isinstance(self._help, (types.FunctionType, types.MethodType)):
103 return self._help()
104 return self._help
106 # Create HTML-form elements that represent a given
107 # value and let the user edit that value. The varprefix
108 # is prepended to the HTML variable names and is needed
109 # in order to make the variable unique in case that another
110 # Value of the same type is being used as well.
111 # The function may assume that the type of the value is valid.
112 def render_input(self, varprefix, value):
113 pass
115 # Sets the input focus (cursor) into the most promiment
116 # field of the HTML code previously rendered with render_input()
117 def set_focus(self, varprefix):
118 html.set_focus(varprefix)
120 # Create a canonical, minimal, default value that
121 # matches the datatype of the value specification and
122 # fulfills also data validation.
123 def canonical_value(self):
124 return None
126 # Return a default value for this variable. This
127 # is optional and only used in the value editor
128 # for same cases where the default value is known.
129 def default_value(self):
130 try:
131 if isinstance(self._default_value, (types.FunctionType, types.MethodType)):
132 return self._default_value()
133 return self._default_value
134 except:
135 return self.canonical_value()
137 # Creates a text-representation of the value that can be
138 # used in tables and other contextes. It is to be read
139 # by the user and need not to be parsable.
140 # The function may assume that the type of the value is valid.
142 # In the current implementation this function is only used to
143 # render the object for html code. So it is allowed to add
144 # html code for better layout in the GUI.
145 def value_to_text(self, value):
146 return repr(value)
148 # Create a value from the current settings of the
149 # HTML variables. This function must also check the validity
150 # and may raise a MKUserError in case of invalid set variables.
151 def from_html_vars(self, varprefix):
152 return None
154 # Check if a given value matches the
155 # datatype of described by this class. This method will
156 # be used by cmk -X on the command line in order to
157 # validate main.mk (some happy day in future)
158 def validate_datatype(self, value, varprefix):
159 pass
161 # Check if a given value is within the ranges that are
162 # allowed for this type of value. This function should
163 # assume that the data type is valid (either because it
164 # has been returned by from_html_vars() or because it has
165 # been checked with validate_datatype()).
166 def validate_value(self, value, varprefix):
167 self.custom_validate(value, varprefix)
169 # Needed for implementation of customer validation
170 # functions that are configured by the user argument
171 # validate = .... Problem: this function must be
172 # called by *every* validate_value() function in all
173 # subclasses - explicitely.
174 def custom_validate(self, value, varprefix):
175 if self._validate:
176 self._validate(value, varprefix)
178 def classtype_info(self):
179 superclass = " ".join(base.__name__ for base in self.__class__.__bases__)
180 if not self.debug_display:
181 return
182 html.div("Check_MK-Type: %s %s" % (superclass, type(self).__name__), class_="legend")
185 # A fixed non-editable value, e.g. to be use in "Alternative"
186 class FixedValue(ValueSpec):
187 def __init__(self, value, **kwargs):
188 ValueSpec.__init__(self, **kwargs)
189 self._value = value
190 self._totext = kwargs.get("totext")
192 def canonical_value(self):
193 return self._value
195 def render_input(self, varprefix, value):
196 html.write(self.value_to_text(value))
198 def value_to_text(self, value):
199 if self._totext is not None:
200 return self._totext
201 elif isinstance(value, unicode):
202 return value
203 return str(value)
205 def from_html_vars(self, varprefix):
206 return self._value
208 def validate_datatype(self, value, varprefix):
209 if not self._value == value:
210 raise MKUserError(varprefix,
211 _("Invalid value, must be '%r' but is '%r'") % (self._value, value))
213 def validate_value(self, value, varprefix):
214 self.validate_datatype(value, varprefix)
215 ValueSpec.custom_validate(self, value, varprefix)
218 # Time in seconds
219 class Age(ValueSpec):
220 def __init__(self, **kwargs):
221 ValueSpec.__init__(self, **kwargs)
222 self._label = kwargs.get("label")
223 self._minvalue = kwargs.get("minvalue")
224 self._display = kwargs.get("display", ["days", "hours", "minutes", "seconds"])
226 def canonical_value(self):
227 if self._minvalue:
228 return self._minvalue
229 return 0
231 def render_input(self, varprefix, value):
232 self.classtype_info()
233 days, rest = divmod(value, 60 * 60 * 24)
234 hours, rest = divmod(rest, 60 * 60)
235 minutes, seconds = divmod(rest, 60)
237 html.open_div()
238 if self._label:
239 html.write(self._label + " ")
241 takeover = 0
242 first = True
243 for uid, title, val, tkovr_fac in [("days", _("days"), days, 24),
244 ("hours", _("hours"), hours, 60),
245 ("minutes", _("mins"), minutes, 60),
246 ("seconds", _("secs"), seconds, 60)]:
247 if uid in self._display:
248 val += takeover
249 takeover = 0
250 html.number_input(varprefix + "_" + uid, val, 3 if first else 2)
251 html.write(" %s " % title)
252 first = False
253 else:
254 takeover = (takeover + val) * tkovr_fac
255 html.close_div()
257 def from_html_vars(self, varprefix):
258 # TODO: Validate for correct numbers!
259 return (utils.saveint(html.request.var(varprefix + '_days', 0)) * 3600 * 24 +
260 utils.saveint(html.request.var(varprefix + '_hours', 0)) * 3600 +
261 utils.saveint(html.request.var(varprefix + '_minutes', 0)) * 60 + utils.saveint(
262 html.request.var(varprefix + '_seconds', 0)))
264 def value_to_text(self, value):
265 days, rest = divmod(value, 60 * 60 * 24)
266 hours, rest = divmod(rest, 60 * 60)
267 minutes, seconds = divmod(rest, 60)
268 parts = []
269 for title, count in [
270 (_("days"), days),
271 (_("hours"), hours),
272 (_("minutes"), minutes),
273 (_("seconds"), seconds),
275 if count:
276 parts.append("%d %s" % (count, title))
278 if parts:
279 return " ".join(parts)
280 return _("no time")
282 def validate_datatype(self, value, varprefix):
283 if not isinstance(value, int):
284 raise MKUserError(
285 varprefix,
286 _("The value %r has type %s, but must be of type int") % (value, _type_name(value)))
288 def validate_value(self, value, varprefix):
289 if self._minvalue is not None and value < self._minvalue:
290 raise MKUserError(
291 varprefix,
292 _("%s is too low. The minimum allowed value is %s.") % (value, self._minvalue))
295 # Editor for a single integer
296 class Integer(ValueSpec):
297 def __init__(self, **kwargs):
298 ValueSpec.__init__(self, **kwargs)
299 self._size = kwargs.get("size", 5)
300 # TODO: inconsistency with default_value. All should be named with underscore
301 self._minvalue = kwargs.get("minvalue")
302 self._maxvalue = kwargs.get("maxvalue")
303 self._label = kwargs.get("label")
304 self._unit = kwargs.get("unit", "")
305 self._thousand_sep = kwargs.get("thousand_sep")
306 self._display_format = kwargs.get("display_format", "%d")
307 self._align = kwargs.get("align", "left")
309 if "size" not in kwargs and "maxvalue" in kwargs and kwargs["maxvalue"] is not None:
310 self._size = 1 + int(math.log10(self._maxvalue)) + \
311 (3 if isinstance(self._maxvalue, float) else 0)
313 def canonical_value(self):
314 if self._minvalue:
315 return self._minvalue
316 return 0
318 def render_input(self, varprefix, value):
319 self.classtype_info()
320 if self._label:
321 html.write(self._label)
322 html.nbsp()
323 if self._align == "right":
324 style = "text-align: right;"
325 else:
326 style = ""
327 if value == "": # This is needed for ListOfIntegers
328 html.text_input(varprefix, "", "number", size=self._size, style=style)
329 else:
330 html.number_input(varprefix, self._render_value(value), size=self._size, style=style)
331 if self._unit:
332 html.nbsp()
333 html.write(self._unit)
335 def _render_value(self, value):
336 return self._display_format % utils.saveint(value)
338 def from_html_vars(self, varprefix):
339 try:
340 return int(html.request.var(varprefix))
341 except:
342 raise MKUserError(
343 varprefix,
344 _("The text <b><tt>%s</tt></b> is not a valid integer number.") %
345 html.request.var(varprefix))
347 def value_to_text(self, value):
348 text = self._display_format % value
349 if self._thousand_sep:
350 sepped = ""
351 rest = text
352 while len(rest) > 3:
353 sepped = self._thousand_sep + rest[-3:] + sepped
354 rest = rest[:-3]
355 sepped = rest + sepped
356 text = sepped
358 if self._unit:
359 text += "&nbsp;" + self._unit
360 return text
362 def validate_datatype(self, value, varprefix):
363 if not isinstance(value, numbers.Integral):
364 raise MKUserError(
365 varprefix,
366 _("The value %r has the wrong type %s, but must be of type int") %
367 (value, _type_name(value)))
369 def validate_value(self, value, varprefix):
370 if self._minvalue is not None and value < self._minvalue:
371 raise MKUserError(
372 varprefix,
373 _("%s is too low. The minimum allowed value is %s.") % (value, self._minvalue))
374 if self._maxvalue is not None and value > self._maxvalue:
375 raise MKUserError(
376 varprefix,
377 _("%s is too high. The maximum allowed value is %s.") % (value, self._maxvalue))
378 ValueSpec.custom_validate(self, value, varprefix)
381 # Filesize in Byte, KByte, MByte, Gigabyte, Terabyte
382 class Filesize(Integer):
383 def __init__(self, **kwargs):
384 Integer.__init__(self, **kwargs)
385 self._names = ['Byte', 'KByte', 'MByte', 'GByte', 'TByte']
387 def get_exponent(self, value):
388 for exp, count in ((exp, 1024**exp) for exp in reversed(xrange(len(self._names)))):
389 if value == 0:
390 return 0, 0
391 if value % count == 0:
392 return exp, value / count
394 def render_input(self, varprefix, value):
395 self.classtype_info()
396 exp, count = self.get_exponent(value)
397 html.number_input(varprefix + '_size', count, size=self._size)
398 html.nbsp()
399 choices = [(str(nr), name) for (nr, name) in enumerate(self._names)]
400 html.dropdown(varprefix + '_unit', choices, deflt=str(exp))
402 def from_html_vars(self, varprefix):
403 try:
404 return int(html.request.var(varprefix + '_size')) * (1024**int(
405 html.request.var(varprefix + '_unit')))
406 except:
407 raise MKUserError(varprefix + '_size', _("Please enter a valid integer number"))
409 def value_to_text(self, value):
410 exp, count = self.get_exponent(value)
411 return "%s %s" % (count, self._names[exp])
414 # Editor for a line of text
415 class TextAscii(ValueSpec):
416 def __init__(self, **kwargs):
417 super(TextAscii, self).__init__(**kwargs)
418 self._label = kwargs.get("label")
419 self._size = kwargs.get("size", 25) # also possible: "max"
420 self._try_max_width = kwargs.get("try_max_width", False) # If set, uses calc(100%-10px)
421 self._cssclass = kwargs.get("cssclass", "text")
422 self._strip = kwargs.get("strip", True)
423 self._attrencode = kwargs.get("attrencode", True)
424 self._allow_empty = kwargs.get("allow_empty", _("none"))
425 self._empty_text = kwargs.get("empty_text", "")
426 self._read_only = kwargs.get("read_only")
427 self._none_is_empty = kwargs.get("none_is_empty", False)
428 self._forbidden_chars = kwargs.get("forbidden_chars", "")
429 self._regex = kwargs.get("regex")
430 self._regex_error = kwargs.get("regex_error",
431 _("Your input does not match the required format."))
432 self._minlen = kwargs.get('minlen', None)
433 if isinstance(self._regex, str):
434 self._regex = re.compile(self._regex)
435 self._onkeyup = kwargs.get("onkeyup")
436 self._autocomplete = kwargs.get("autocomplete", True)
437 self._hidden = kwargs.get("hidden", False)
439 def canonical_value(self):
440 return ""
442 def render_input(self, varprefix, value):
443 self.classtype_info()
444 value_text = "%s" % value if value is not None else ""
446 if self._label:
447 html.write(self._label)
448 html.nbsp()
450 type_ = "password" if self._hidden else "text"
452 attrs = {}
453 if self._onkeyup:
454 attrs["onkeyup"] = self._onkeyup
456 html.text_input(
457 varprefix,
458 value_text,
459 size=self._size,
460 try_max_width=self._try_max_width,
461 read_only=self._read_only,
462 cssclass=self._cssclass,
463 type=type_,
464 attrs=attrs,
465 autocomplete="off" if not self._autocomplete else None,
468 def value_to_text(self, value):
469 if not value:
470 return self._empty_text
472 if self._attrencode:
473 return html.attrencode(value)
474 return value
476 def from_html_vars(self, varprefix):
477 value = html.request.var(varprefix, "")
478 if self._strip:
479 value = value.strip()
480 if self._none_is_empty and not value:
481 return None
482 return value
484 def validate_datatype(self, value, varprefix):
485 if self._none_is_empty and value is None:
486 return
488 if not isinstance(value, str):
489 raise MKUserError(
490 varprefix,
491 _("The value must be of type str, but it has type %s") % _type_name(value))
493 def validate_value(self, value, varprefix):
494 try:
495 unicode(value)
496 except:
497 raise MKUserError(varprefix, _("Non-ASCII characters are not allowed here."))
498 if self._forbidden_chars:
499 for c in self._forbidden_chars:
500 if c in value:
501 raise MKUserError(varprefix,
502 _("The character <tt>%s</tt> is not allowed here.") % c)
503 if self._none_is_empty and value == "":
504 raise MKUserError(varprefix, _("An empty value must be represented with None here."))
505 if not self._allow_empty and value.strip() == "":
506 raise MKUserError(varprefix, _("An empty value is not allowed here."))
507 if value and self._regex:
508 if not self._regex.match(value):
509 raise MKUserError(varprefix, self._regex_error)
511 if self._minlen is not None and len(value) < self._minlen:
512 raise MKUserError(varprefix,
513 _("You need to provide at least %d characters.") % self._minlen)
515 ValueSpec.custom_validate(self, value, varprefix)
518 class TextUnicode(TextAscii):
519 def __init__(self, **kwargs):
520 TextAscii.__init__(self, **kwargs)
522 def from_html_vars(self, varprefix):
523 return html.get_unicode_input(varprefix, "").strip()
525 def validate_datatype(self, value, varprefix):
526 if not isinstance(value, six.string_types):
527 raise MKUserError(
528 varprefix,
529 _("The value must be of type str or unicode, but it has type %s") %
530 _type_name(value))
533 # Internal ID as used in many places (for contact names, group name,
534 # an so on)
535 class ID(TextAscii):
536 def __init__(self, **kwargs):
537 TextAscii.__init__(self, **kwargs)
538 self._regex = re.compile('^[a-zA-Z_][-a-zA-Z0-9_]*$')
539 self._regex_error = _("An identifier must only consist of letters, digits, dash and "
540 "underscore and it must start with a letter or underscore.")
543 # Same as the ID class, but allowing unicode objects
544 class UnicodeID(TextUnicode):
545 def __init__(self, **kwargs):
546 TextUnicode.__init__(self, **kwargs)
547 self._regex = re.compile(r'^[\w][-\w0-9_]*$', re.UNICODE)
548 self._regex_error = _("An identifier must only consist of letters, digits, dash and "
549 "underscore and it must start with a letter or underscore.")
552 class UserID(TextUnicode):
553 def __init__(self, **kwargs):
554 TextUnicode.__init__(self, **kwargs)
555 self._regex = re.compile(r'^[\w][-\w0-9_\.@]*$', re.UNICODE)
556 self._regex_error = _("An identifier must only consist of letters, digits, dash, dot, "
557 "at and underscore. But it must start with a letter or underscore.")
560 class RegExp(TextAscii):
561 infix = "infix"
562 prefix = "prefix"
563 complete = "complete"
565 def __init__(self, mode, **kwargs):
566 self._mode = mode
567 self._case_sensitive = kwargs.get("case_sensitive", True)
569 TextAscii.__init__(self, attrencode=True, cssclass=self._css_classes(), **kwargs)
571 self._mingroups = kwargs.get("mingroups", 0)
572 self._maxgroups = kwargs.get("maxgroups")
574 def help(self):
575 help_text = []
577 default_help_text = TextAscii.help(self)
578 if default_help_text is not None:
579 help_text.append(default_help_text + "<br><br>")
581 help_text.append(_("The text entered here is handled as a regular expression pattern."))
583 if self._mode == RegExp.infix:
584 help_text.append(
585 _("The pattern is applied as infix search. Add a leading <tt>^</tt> "
586 "to make it match from the beginning and/or a tailing <tt>$</tt> "
587 "to match till the end of the text."))
588 elif self._mode == RegExp.prefix:
589 help_text.append(
590 _("The pattern is matched from the beginning. Add a tailing "
591 "<tt>$</tt> to change it to a whole text match."))
592 elif self._mode == RegExp.complete:
593 help_text.append(
594 _("The pattern is matching the whole text. You can add <tt>.*</tt> "
595 "in front or at the end of your pattern to make it either a prefix "
596 "or infix search."))
598 if self._case_sensitive is True:
599 help_text.append(_("The match is performed case sensitive."))
600 elif self._case_sensitive is False:
601 help_text.append(_("The match is performed case insensitive."))
603 help_text.append(
604 _("Please note that any backslashes need to be escaped using a backslash, "
605 "for example you need to insert <tt>C:\\\\windows\\\\</tt> if you want to match "
606 "<tt>c:\\windows\\</tt>."))
608 return " ".join(help_text)
610 def _css_classes(self):
611 classes = ["text", "regexp"]
613 if self._case_sensitive is True:
614 classes.append("case_sensitive")
615 elif self._case_sensitive is False:
616 classes.append("case_insensitive")
618 if self._mode is not None:
619 classes.append(self._mode)
621 return " ".join(classes)
623 def validate_value(self, value, varprefix):
624 TextAscii.validate_value(self, value, varprefix)
626 # Check if the string is a valid regex
627 try:
628 compiled = re.compile(value)
629 except sre_constants.error as e:
630 raise MKUserError(varprefix, _('Invalid regular expression: %s') % e)
632 if compiled.groups < self._mingroups:
633 raise MKUserError(
634 varprefix,
635 _("Your regular expression containes <b>%d</b> groups. "
636 "You need at least <b>%d</b> groups.") % (compiled.groups, self._mingroups))
637 if self._maxgroups is not None and compiled.groups > self._maxgroups:
638 raise MKUserError(
639 varprefix,
640 _("Your regular expression containes <b>%d</b> groups. "
641 "It must have at most <b>%d</b> groups.") % (compiled.groups, self._maxgroups))
643 ValueSpec.custom_validate(self, value, varprefix)
646 class RegExpUnicode(TextUnicode, RegExp):
647 def __init__(self, **kwargs):
648 TextUnicode.__init__(self, attrencode=True, **kwargs)
649 RegExp.__init__(self, **kwargs)
651 def validate_value(self, value, varprefix):
652 TextUnicode.validate_value(self, value, varprefix)
653 RegExp.validate_value(self, value, varprefix)
654 ValueSpec.custom_validate(self, value, varprefix)
657 class EmailAddress(TextAscii):
658 def __init__(self, **kwargs):
659 kwargs.setdefault("size", 40)
660 TextAscii.__init__(self, **kwargs)
661 # The "new" top level domains are very unlimited in length. Theoretically they can be
662 # up to 63 chars long. But currently the longest is 24 characters. Check this out with:
663 # wget -qO - http://data.iana.org/TLD/tlds-alpha-by-domain.txt | tail -n+2 | wc -L
664 self._regex = re.compile(r'^[A-Z0-9._%&+-]+@(localhost|[A-Z0-9.-]+\.[A-Z]{2,24})$', re.I)
665 self._make_clickable = kwargs.get("make_clickable", False)
667 def value_to_text(self, value):
668 if not value:
669 return TextAscii.value_to_text(self, value)
670 elif self._make_clickable:
671 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
672 return "%s" % html.render_a(HTML(value), href="mailto:%s" % value)
673 return value
676 class EmailAddressUnicode(TextUnicode, EmailAddress):
677 def __init__(self, **kwargs):
678 TextUnicode.__init__(self, **kwargs)
679 EmailAddress.__init__(self, **kwargs)
680 self._regex = re.compile(r'^[\w.%&+-]+@(localhost|[\w.-]+\.[\w]{2,24})$', re.I | re.UNICODE)
682 def validate_value(self, value, varprefix):
683 TextUnicode.validate_value(self, value, varprefix)
684 EmailAddress.validate_value(self, value, varprefix)
685 ValueSpec.custom_validate(self, value, varprefix)
688 # Same as IPv4Network, but allowing both IPv4 and IPv6
689 class IPNetwork(TextAscii):
690 def __init__(self, **kwargs):
691 kwargs.setdefault("size", 34)
692 super(IPNetwork, self).__init__(**kwargs)
694 def _ip_class(self):
695 return ipaddress.ip_interface
697 def validate_value(self, value, varprefix):
698 super(IPNetwork, self).validate_value(value, varprefix)
700 try:
701 self._ip_class()(value.decode("utf-8"))
702 except ValueError as e:
703 raise MKUserError(varprefix, _("Invalid address: %s") % e)
706 # Network as used in routing configuration, such as
707 # "10.0.0.0/8" or "192.168.56.1"
708 class IPv4Network(IPNetwork):
709 def __init__(self, **kwargs):
710 kwargs.setdefault("size", 18)
711 super(IPv4Network, self).__init__(**kwargs)
713 def _ip_class(self):
714 return ipaddress.IPv4Interface
717 class IPv4Address(IPNetwork):
718 def __init__(self, **kwargs):
719 kwargs.setdefault("size", 16)
720 super(IPv4Address, self).__init__(**kwargs)
722 def _ip_class(self):
723 return ipaddress.IPv4Address
726 class TextAsciiAutocomplete(TextAscii):
727 def __init__(self, completion_ident, completion_params, **kwargs):
728 kwargs["onkeyup"] = "cmk.valuespecs.autocomplete(this, %s, %s, %s);%s" % \
729 (json.dumps(completion_ident),
730 json.dumps(completion_params),
731 json.dumps(kwargs.get("onkeyup")),
732 kwargs.get("onkeyup", ""))
733 kwargs["autocomplete"] = False
734 super(TextAsciiAutocomplete, self).__init__(**kwargs)
736 @classmethod
737 def idents(cls):
738 idents = {}
739 for type_class in cls.__subclasses__(): # pylint: disable=no-member
740 idents[type_class.ident] = type_class
741 return idents
743 @classmethod
744 def ajax_handler(cls):
745 ident = html.request.var("ident")
746 if not ident:
747 raise MKUserError("ident", _("You need to set the \"%s\" parameter.") % "ident")
749 if ident not in cls.idents():
750 raise MKUserError("ident", _("Invalid ident: %s") % ident)
752 raw_params = html.request.var("params")
753 if not raw_params:
754 raise MKUserError("params", _("You need to set the \"%s\" parameter.") % "params")
756 try:
757 params = json.loads(raw_params)
758 except ValueError as e: # Python 3: json.JSONDecodeError
759 raise MKUserError("params", _("Invalid parameters: %s") % e)
761 value = html.request.var("value")
762 if value is None:
763 raise MKUserError("params", _("You need to set the \"%s\" parameter.") % "value")
765 result_data = cls.idents()[ident].autocomplete_choices(value, params)
767 # Check for correct result_data format
768 assert isinstance(result_data, list)
769 if result_data:
770 assert isinstance(result_data[0], (list, tuple))
771 assert len(result_data[0]) == 2
773 html.write(json.dumps(result_data))
775 #@abc.abstractclassmethod
776 @classmethod
777 def autocomplete_choices(cls, value, params):
778 raise NotImplementedError()
781 class MonitoredHostname(TextAsciiAutocomplete):
782 """Hostname input with dropdown completion
784 Renders an input field for entering a host name while providing an auto completion dropdown field.
785 Fetching the choices from the current live config via livestatus"""
786 ident = "monitored_hostname"
788 def __init__(self, **kwargs):
789 super(MonitoredHostname, self).__init__(
790 completion_ident=self.ident, completion_params={}, **kwargs)
792 @classmethod
793 def autocomplete_choices(cls, value, params):
794 """Return the matching list of dropdown choices
795 Called by the webservice with the current input field value and the completions_params to get the list of choices"""
796 import cmk.gui.sites as sites
798 query = ("GET hosts\n"
799 "Columns: host_name\n"
800 "Filter: host_name ~~ %s" % livestatus.lqencode(value))
801 hosts = sorted(sites.live().query_column_unique(query))
803 return [(h, h) for h in hosts]
806 @page_registry.register_page("ajax_vs_autocomplete")
807 class PageVsAutocomplete(Page):
808 def page(self):
809 # TODO: Move ajax_handler to this class? Should we also move the autocomplete_choices()?
810 TextAsciiAutocomplete.ajax_handler()
813 # A host name with or without domain part. Also allow IP addresses
814 class Hostname(TextAscii):
815 def __init__(self, **kwargs):
816 TextAscii.__init__(self, **kwargs)
817 self._regex = re.compile('^[-0-9a-zA-Z_.]+$')
818 self._regex_error = _("Please enter a valid hostname or IPv4 address. "
819 "Only letters, digits, dash, underscore and dot are allowed.")
820 if "allow_empty" not in kwargs:
821 self._allow_empty = False
824 # Use this for all host / ip address input fields!
825 class HostAddress(TextAscii):
826 def __init__(self, **kwargs):
827 TextAscii.__init__(self, **kwargs)
828 self._allow_host_name = kwargs.get("allow_host_name", True)
829 self._allow_ipv4_address = kwargs.get("allow_ipv4_address", True)
830 self._allow_ipv6_address = kwargs.get("allow_ipv6_address", True)
832 def validate_value(self, value, varprefix):
833 if value and self._allow_host_name and self._is_valid_host_name(value):
834 pass
835 elif value and self._allow_ipv4_address and self._is_valid_ipv4_address(value):
836 pass
837 elif value and self._allow_ipv6_address and self._is_valid_ipv6_address(value):
838 pass
839 elif not self._allow_empty:
840 raise MKUserError(
841 varprefix,
842 _("Invalid host address. You need to specify the address "
843 "either as %s.") % ", ".join(self._allowed_type_names()))
845 ValueSpec.custom_validate(self, value, varprefix)
847 def _is_valid_host_name(self, hostname):
848 # http://stackoverflow.com/questions/2532053/validate-a-hostname-string/2532344#2532344
849 if len(hostname) > 255:
850 return False
852 if hostname[-1] == ".":
853 hostname = hostname[:-1] # strip exactly one dot from the right, if present
855 # must be not all-numeric, so that it can't be confused with an IPv4 address.
856 # Host names may start with numbers (RFC 1123 section 2.1) but never the final part,
857 # since TLDs are alphabetic.
858 if re.match(r"[\d.]+$", hostname):
859 return False
861 allowed = re.compile(r"(?!-)[A-Z_\d-]{1,63}(?<!-)$", re.IGNORECASE)
862 return all(allowed.match(x) for x in hostname.split("."))
864 def _is_valid_ipv4_address(self, address):
865 # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python/4017219#4017219
866 try:
867 socket.inet_pton(socket.AF_INET, address)
868 except AttributeError: # no inet_pton here, sorry
869 try:
870 socket.inet_aton(address)
871 except socket.error:
872 return False
874 return address.count('.') == 3
876 except socket.error: # not a valid address
877 return False
879 return True
881 def _is_valid_ipv6_address(self, address):
882 # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python/4017219#4017219
883 try:
884 socket.inet_pton(socket.AF_INET6, address)
885 except socket.error: # not a valid address
886 return False
887 return True
889 def _allowed_type_names(self):
890 allowed = []
891 if self._allow_host_name:
892 allowed.append(_("Host- or DNS name"))
894 if self._allow_ipv4_address:
895 allowed.append(_("IPv4 address"))
897 if self._allow_ipv6_address:
898 allowed.append(_("IPv6 address"))
900 return allowed
903 class AbsoluteDirname(TextAscii):
904 def __init__(self, **kwargs):
905 TextAscii.__init__(self, **kwargs)
906 self._regex = re.compile('^(/|(/[^/]+)+)$')
907 self._regex_error = _("Please enter a valid absolut pathname with / as a path separator.")
910 class Url(TextAscii):
911 def __init__(self, default_scheme, allowed_schemes, **kwargs):
912 kwargs.setdefault("size", 64)
913 self._default_scheme = default_scheme
914 self._allowed_schemes = allowed_schemes
915 self._show_as_link = kwargs.get("show_as_link", False)
916 self._link_target = kwargs.get("target")
917 super(Url, self).__init__(**kwargs)
919 def validate_value(self, value, varprefix):
920 super(Url, self).validate_value(value, varprefix)
922 if self._allow_empty and value == "":
923 ValueSpec.custom_validate(self, value, varprefix)
924 return
926 parts = urlparse.urlparse(value)
927 if not parts.scheme or not parts.netloc:
928 raise MKUserError(varprefix, _("Invalid URL given"))
930 if parts.scheme not in self._allowed_schemes:
931 raise MKUserError(
932 varprefix,
933 _("Invalid URL scheme. Must be one of: %s") % ", ".join(self._allowed_schemes))
935 ValueSpec.custom_validate(self, value, varprefix)
937 def from_html_vars(self, varprefix):
938 value = super(Url, self).from_html_vars(varprefix)
939 if value and "://" not in value:
940 value = self._default_scheme + "://" + value
941 return value
943 def value_to_text(self, value):
944 if not any(value.startswith(scheme + "://") for scheme in self._allowed_schemes):
945 value = self._default_scheme + "://" + value
947 try:
948 parts = urlparse.urlparse(value)
949 if parts.path in ['', '/']:
950 text = parts.netloc
951 else:
952 text = parts.netloc + parts.path
953 except:
954 text = value[7:]
956 # Remove trailing / if the url does not contain any path component
957 if self._show_as_link:
958 return html.render_a(
959 text, href=value, target=self._link_target if self._link_target else None)
961 return value
964 class HTTPUrl(Url):
965 """Valuespec for a HTTP or HTTPS Url, that automatically adds http:// to the value if no scheme has been specified"""
967 def __init__(self, **kwargs):
968 kwargs.setdefault("show_as_link", True)
969 super(HTTPUrl, self).__init__(
970 allowed_schemes=["http", "https"], default_scheme="http", **kwargs)
973 def CheckMKVersion(**args):
974 args = args.copy()
975 args["regex"] = r"[0-9]+\.[0-9]+\.[0-9]+([bpi][0-9]+|i[0-9]+p[0-9]+)?$"
976 args["regex_error"] = _("This is not a valid Check_MK version number")
977 return TextAscii(**args)
980 class TextAreaUnicode(TextUnicode):
981 def __init__(self, **kwargs):
982 TextUnicode.__init__(self, **kwargs)
983 self._cols = kwargs.get("cols", 60)
984 self._try_max_width = kwargs.get("try_max_width", False) # If set, uses calc(100%-10px)
985 self._rows = kwargs.get("rows", 20) # Allowed: "auto" -> Auto resizing
986 self._minrows = kwargs.get("minrows", 0) # Minimum number of initial rows when "auto"
987 self._monospaced = kwargs.get("monospaced", False) # select TT font
989 def value_to_text(self, value):
990 if self._monospaced:
991 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
992 return "%s" % html.render_pre(HTML(value), class_="ve_textarea")
993 return html.attrencode(value).replace("\n", "<br>")
995 def render_input(self, varprefix, value):
996 self.classtype_info()
997 if value is None:
998 value = "" # should never happen, but avoids exception for invalid input
999 if self._rows == "auto":
1000 func = 'cmk.valuespecs.textarea_resize(this, %s);' % json.dumps(html.get_theme())
1001 attrs = {"onkeyup": func, "onmousedown": func, "onmouseup": func, "onmouseout": func}
1002 if html.request.has_var(varprefix):
1003 rows = len(html.request.var(varprefix).splitlines())
1004 else:
1005 rows = len(value.splitlines())
1006 rows = max(rows, self._minrows)
1007 else:
1008 attrs = {}
1009 rows = self._rows
1011 if self._monospaced:
1012 attrs["class"] = "tt"
1014 html.text_area(
1015 varprefix,
1016 value,
1017 rows=rows,
1018 cols=self._cols,
1019 attrs=attrs,
1020 try_max_width=self._try_max_width)
1022 # Overridded because we do not want to strip() here and remove '\r'
1023 def from_html_vars(self, varprefix):
1024 text = html.get_unicode_input(varprefix, "").replace('\r', '')
1025 if text and not text.endswith("\n"):
1026 text += "\n" # force newline at end
1027 return text
1030 # A variant of TextAscii() that validates a path to a filename that
1031 # lies in an existing directory.
1032 # TODO: Rename the valuespec here to ExistingFilename or somehting similar
1033 class Filename(TextAscii):
1034 def __init__(self, **kwargs):
1035 TextAscii.__init__(self, **kwargs)
1036 if "size" not in kwargs:
1037 self._size = 60
1038 if "default" in kwargs:
1039 self._default_path = kwargs["default"]
1040 else:
1041 self._default_path = "/tmp/foo"
1042 if "trans_func" in kwargs:
1043 self._trans_func = kwargs["trans_func"]
1044 else:
1045 self._trans_func = None
1047 def canonical_value(self):
1048 return self._default_path
1050 def validate_value(self, value, varprefix):
1051 # The transformation function only changes the value for validation. This is
1052 # usually a function which is later also used within the code which uses
1053 # this variable to e.g. replace macros
1054 if self._trans_func:
1055 value = self._trans_func(value)
1057 if len(value) == 0:
1058 raise MKUserError(varprefix, _("Please enter a filename."))
1060 if value[0] != "/":
1061 raise MKUserError(
1062 varprefix,
1063 _("Sorry, only absolute filenames are allowed. "
1064 "Your filename must begin with a slash."))
1065 if value[-1] == "/":
1066 raise MKUserError(varprefix, _("Your filename must not end with a slash."))
1068 directory = value.rsplit("/", 1)[0]
1069 if not os.path.isdir(directory):
1070 raise MKUserError(
1071 varprefix,
1072 _("The directory %s does not exist or is not a directory.") % directory)
1074 # Write permissions to the file cannot be checked here since we run with Apache
1075 # permissions and the file might be created with Nagios permissions (on OMD this
1076 # is the same, but for others not)
1078 ValueSpec.custom_validate(self, value, varprefix)
1081 class ListOfStrings(ValueSpec):
1082 def __init__(self, **kwargs):
1083 ValueSpec.__init__(self, **kwargs)
1084 if "valuespec" in kwargs:
1085 self._valuespec = kwargs.get("valuespec")
1086 elif "size" in kwargs:
1087 self._valuespec = TextAscii(size=kwargs["size"])
1088 else:
1089 self._valuespec = TextAscii()
1090 self._vertical = kwargs.get("orientation", "vertical") == "vertical"
1091 self._allow_empty = kwargs.get("allow_empty", True)
1092 self._empty_text = kwargs.get("empty_text", "")
1093 self._max_entries = kwargs.get("max_entries")
1094 self._separator = kwargs.get("separator", "") # in case of float
1096 self._split_on_paste = kwargs.get("split_on_paste", True)
1097 self._split_separators = kwargs.get("split_separators", ";")
1099 def help(self):
1100 help_texts = [
1101 ValueSpec.help(self),
1102 self._valuespec.help(),
1105 if self._split_on_paste:
1106 help_texts.append(
1107 _("You may paste a text from your clipboard which contains several "
1108 "parts separated by \"%s\" characters into the last input field. The text will "
1109 "then be split by these separators and the single parts are added into dedicated "
1110 "input fields.") % self._split_separators)
1112 return " ".join([t for t in help_texts if t])
1114 def render_input(self, varprefix, value):
1115 self.classtype_info()
1116 # Form already submitted?
1117 if html.request.has_var(varprefix + "_0"):
1118 value = self.from_html_vars(varprefix)
1119 # Remove variables from URL, so that they do not appear
1120 # in hidden_fields()
1121 nr = 0
1122 while html.request.has_var(varprefix + "_%d" % nr):
1123 html.request.del_var(varprefix + "_%d" % nr)
1124 nr += 1
1126 class_ = ["listofstrings"]
1127 if self._vertical:
1128 class_.append("vertical")
1129 else:
1130 class_.append("horizontal")
1131 html.open_div(id_=varprefix, class_=class_)
1133 for nr, s in enumerate(value + [""]):
1134 html.open_div()
1135 self._valuespec.render_input(varprefix + "_%d" % nr, s)
1136 if self._vertical != "vertical" and self._separator:
1137 html.nbsp()
1138 html.write(self._separator)
1139 html.nbsp()
1140 html.close_div()
1141 html.close_div()
1142 html.div('', style="clear:left;")
1143 html.help(self.help())
1144 html.javascript("cmk.valuespecs.list_of_strings_init(%s, %s, %s);" %
1145 (json.dumps(varprefix), json.dumps(self._split_on_paste),
1146 json.dumps(self._split_separators)))
1148 def canonical_value(self):
1149 return []
1151 def value_to_text(self, value):
1152 if not value:
1153 return self._empty_text
1155 if self._vertical:
1156 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
1157 s = [
1158 html.render_tr(html.render_td(HTML(self._valuespec.value_to_text(v))))
1159 for v in value
1161 return "%s" % html.render_table(HTML().join(s))
1162 return ", ".join([self._valuespec.value_to_text(v) for v in value])
1164 def from_html_vars(self, varprefix):
1165 value = []
1166 nr = 0
1167 while True:
1168 varname = varprefix + "_%d" % nr
1169 if not html.request.has_var(varname):
1170 break
1171 if html.request.var(varname, "").strip():
1172 value.append(self._valuespec.from_html_vars(varname))
1173 nr += 1
1174 return value
1176 def validate_datatype(self, value, varprefix):
1177 if not isinstance(value, list):
1178 raise MKUserError(
1179 varprefix,
1180 _("Expected data type is list, but your type is %s.") % _type_name(value))
1181 for nr, s in enumerate(value):
1182 self._valuespec.validate_datatype(s, varprefix + "_%d" % nr)
1184 def validate_value(self, value, varprefix):
1185 if len(value) == 0 and not self._allow_empty:
1186 if self._empty_text:
1187 msg = self._empty_text
1188 else:
1189 msg = _("Please specify at least one value")
1190 raise MKUserError(varprefix + "_0", msg)
1192 if self._max_entries is not None and len(value) > self._max_entries:
1193 raise MKUserError(varprefix + "_%d" % self._max_entries,
1194 _("You can specify at most %d entries") % self._max_entries)
1196 if self._valuespec:
1197 for nr, s in enumerate(value):
1198 self._valuespec.validate_value(s, varprefix + "_%d" % nr)
1199 ValueSpec.custom_validate(self, value, varprefix)
1202 class ListOfIntegers(ListOfStrings):
1203 def __init__(self, **kwargs):
1204 int_args = {}
1205 for key in ["minvalue", "maxvalue"]:
1206 if key in kwargs:
1207 int_args[key] = kwargs[key]
1208 int_args["display_format"] = "%s"
1209 int_args["convfunc"] = lambda x: x if x == '' else utils.saveint(x)
1210 int_args["minvalue"] = 17
1211 int_args["default_value"] = 34
1212 valuespec = Integer(**int_args)
1213 kwargs["valuespec"] = valuespec
1214 ListOfStrings.__init__(self, **kwargs)
1217 # Generic list-of-valuespec ValueSpec with Javascript-based
1218 # add/delete/move
1219 class ListOf(ValueSpec):
1220 class Style(Enum):
1221 REGULAR = "regular"
1222 FLOATING = "floating"
1224 def __init__(self, valuespec, **kwargs):
1225 ValueSpec.__init__(self, **kwargs)
1226 self._valuespec = valuespec
1227 self._magic = kwargs.get("magic", "@!@")
1228 self._rowlabel = kwargs.get("row_label")
1229 self._add_label = kwargs.get("add_label", _("Add new element"))
1230 self._del_label = kwargs.get("del_label", _("Delete this entry"))
1231 self._movable = kwargs.get("movable", True)
1232 self._style = kwargs.get("style", ListOf.Style.REGULAR)
1233 self._totext = kwargs.get("totext") # pattern with option %d
1234 self._text_if_empty = kwargs.get("text_if_empty", _("No entries"))
1235 self._allow_empty = kwargs.get("allow_empty", True)
1236 self._empty_text = kwargs.get("empty_text") # complain text if empty
1237 # Makes a sort button visible that can be used to sort the list in the GUI
1238 # (without submitting the form). But this currently only works for list of
1239 # tuples that contain input field elements directly. The value of sort_by
1240 # refers to the index of the sort values in the tuple
1241 self._sort_by = kwargs.get("sort_by")
1242 if not self._empty_text:
1243 self._empty_text = _("Please specify at least one entry")
1245 # Implementation idea: we render our element-valuespec
1246 # once in a hidden div that is not evaluated. All occurances
1247 # of a magic string are replaced with the actual number
1248 # of entry, while beginning with 1 (this makes visual
1249 # numbering in labels, etc. possible). The current number
1250 # of entries is stored in the hidden variable 'varprefix'
1251 def render_input(self, varprefix, value):
1252 self.classtype_info()
1254 html.open_div(class_=["valuespec_listof", self._style.value])
1256 # Beware: the 'value' is only the default value in case the form
1257 # has not yet been filled in. In the complain phase we must
1258 # ignore 'value' but reuse the input from the HTML variables -
1259 # even if they are not syntactically correct. Calling from_html_vars
1260 # here is *not* an option since this might not work in case of
1261 # a wrong user input.
1263 # Render reference element for cloning
1264 self._show_reference_entry(varprefix, self._magic, self._valuespec.default_value())
1266 # In the 'complain' phase, where the user already saved the
1267 # form but the validation failed, we must not display the
1268 # original 'value' but take the value from the HTML variables.
1269 if html.request.has_var("%s_count" % varprefix):
1270 count = len(self.get_indexes(varprefix))
1271 value = [None] * count # dummy for the loop
1272 else:
1273 count = len(value)
1275 html.hidden_field(
1276 '%s_count' % varprefix, str(count), id_='%s_count' % varprefix, add_var=True)
1278 self._show_entries(varprefix, value)
1280 html.close_div()
1282 if count:
1283 html.javascript("cmk.valuespecs.listof_update_indices(%s)" % json.dumps(varprefix))
1285 def _show_entries(self, varprefix, value):
1286 if self._style == ListOf.Style.REGULAR:
1287 self._show_current_entries(varprefix, value)
1288 html.br()
1289 self._list_buttons(varprefix)
1291 elif self._style == ListOf.Style.FLOATING:
1292 html.open_table()
1293 html.open_tbody()
1294 html.open_tr()
1295 html.open_td()
1296 self._list_buttons(varprefix)
1297 html.close_td()
1298 html.open_td()
1299 self._show_current_entries(varprefix, value)
1300 html.close_td()
1301 html.close_tr()
1302 html.close_tbody()
1303 html.close_table()
1305 else:
1306 raise NotImplementedError()
1308 def _list_buttons(self, varprefix):
1309 html.jsbutton(
1310 varprefix + "_add", self._add_label, "cmk.valuespecs.listof_add(%s, %s, %s)" %
1311 (json.dumps(varprefix), json.dumps(self._magic), json.dumps(self._style.value)))
1313 if self._sort_by is not None:
1314 html.jsbutton(
1315 varprefix + "_sort", _("Sort"), "cmk.valuespecs.listof_sort(%s, %s, %s)" %
1316 (json.dumps(varprefix), json.dumps(self._magic), json.dumps(self._sort_by)))
1318 def _show_reference_entry(self, varprefix, index, value):
1319 if self._style == ListOf.Style.REGULAR:
1320 html.open_table(style="display:none;")
1321 html.open_tbody(id_="%s_prototype" % varprefix, class_="vlof_prototype")
1323 self._show_entry(varprefix, index, value)
1325 html.close_tbody()
1326 html.close_table()
1328 elif self._style == ListOf.Style.FLOATING:
1329 html.open_div(id_="%s_prototype" % varprefix, style="display:none;")
1331 self._show_entry(varprefix, index, value)
1333 html.close_div()
1335 else:
1336 raise NotImplementedError()
1338 def _show_current_entries(self, varprefix, value):
1339 if self._style == ListOf.Style.REGULAR:
1340 html.open_table(class_=["valuespec_listof"])
1341 html.open_tbody(id_="%s_container" % varprefix)
1343 for nr, v in enumerate(value):
1344 self._show_entry(varprefix, "%d" % (nr + 1), v)
1346 html.close_tbody()
1347 html.close_table()
1349 elif self._style == ListOf.Style.FLOATING:
1350 html.open_div(
1351 id_="%s_container" % varprefix, class_=["valuespec_listof_floating_container"])
1353 for nr, v in enumerate(value):
1354 self._show_entry(varprefix, "%d" % (nr + 1), v)
1356 html.close_div()
1358 else:
1359 raise NotImplementedError()
1361 def _show_entry(self, varprefix, index, value):
1362 entry_id = "%s_entry_%s" % (varprefix, index)
1364 if self._style == ListOf.Style.REGULAR:
1365 html.open_tr(id_=entry_id)
1366 self._show_entry_cell(varprefix, index, value)
1367 html.close_tr()
1369 elif self._style == ListOf.Style.FLOATING:
1370 html.open_table(id_=entry_id)
1371 html.open_tbody()
1372 html.open_tr()
1373 self._show_entry_cell(varprefix, index, value)
1374 html.close_tr()
1375 html.close_tbody()
1376 html.close_table()
1378 else:
1379 raise NotImplementedError()
1381 def _show_entry_cell(self, varprefix, index, value):
1382 html.open_td(class_="vlof_buttons")
1384 html.hidden_field(
1385 varprefix + "_indexof_" + index, "", add_var=True,
1386 class_="index") # reconstruct order after moving stuff
1387 html.hidden_field(
1388 varprefix + "_orig_indexof_" + index, "", add_var=True, class_="orig_index")
1389 if self._movable:
1390 html.element_dragger_js(
1391 "tr",
1392 drop_handler="cmk.valuespecs.listof_drop_handler",
1393 handler_args={
1394 "cur_index": index,
1395 "varprefix": varprefix
1397 self._del_button(varprefix, index)
1398 html.close_td()
1399 html.open_td(class_="vlof_content")
1400 self._valuespec.render_input(varprefix + "_" + index, value)
1401 html.close_td()
1403 def _del_button(self, vp, nr):
1404 js = "cmk.valuespecs.listof_delete(%s, %s)" % (json.dumps(vp), json.dumps(nr))
1405 html.icon_button("#", self._del_label, "delete", onclick=js)
1407 def canonical_value(self):
1408 return []
1410 def value_to_text(self, value):
1411 if self._totext:
1412 if "%d" in self._totext:
1413 return self._totext % len(value)
1414 return self._totext
1415 elif not value:
1416 return self._text_if_empty
1418 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
1419 s = [html.render_tr(html.render_td(HTML(self._valuespec.value_to_text(v)))) for v in value]
1420 return "%s" % html.render_table(HTML().join(s))
1422 def get_indexes(self, varprefix):
1423 count = html.get_integer_input(varprefix + "_count", 0)
1424 n = 1
1425 indexes = {}
1426 while n <= count:
1427 indexof = html.request.var(varprefix + "_indexof_%d" % n)
1428 # for deleted entries, we have removed the whole row, therefore indexof is None
1429 if indexof is not None:
1430 indexes[int(indexof)] = n
1431 n += 1
1432 return indexes
1434 def from_html_vars(self, varprefix):
1435 indexes = self.get_indexes(varprefix)
1436 value = []
1437 k = indexes.keys()
1438 k.sort()
1439 for i in k:
1440 val = self._valuespec.from_html_vars(varprefix + "_%d" % indexes[i])
1441 value.append(val)
1442 return value
1444 def validate_datatype(self, value, varprefix):
1445 if not isinstance(value, list):
1446 raise MKUserError(varprefix, _("The type must be list, but is %s") % _type_name(value))
1447 for n, v in enumerate(value):
1448 self._valuespec.validate_datatype(v, varprefix + "_%d" % (n + 1))
1450 def validate_value(self, value, varprefix):
1451 if not self._allow_empty and len(value) == 0:
1452 raise MKUserError(varprefix, self._empty_text)
1453 for n, v in enumerate(value):
1454 self._valuespec.validate_value(v, varprefix + "_%d" % (n + 1))
1455 ValueSpec.custom_validate(self, value, varprefix)
1458 # A generic valuespec where the user can choose from a list of sub-valuespecs.
1459 # Each sub-valuespec can be added only once
1460 class ListOfMultiple(ValueSpec):
1461 def __init__(self, choices, **kwargs):
1462 ValueSpec.__init__(self, **kwargs)
1463 self._choices = choices
1464 self._choice_dict = dict(choices)
1465 self._size = kwargs.get("size")
1466 self._add_label = kwargs.get("add_label", _("Add element"))
1467 self._del_label = kwargs.get("del_label", _("Delete this entry"))
1468 self._delete_style = kwargs.get("delete_style", "default") # or "filter"
1470 def del_button(self, varprefix, ident):
1471 js = "cmk.valuespecs.listofmultiple_del('%s', '%s')" % (varprefix, ident)
1472 html.icon_button("#", self._del_label, "delete", onclick=js)
1474 def render_input(self, varprefix, value):
1475 self.classtype_info()
1476 # Beware: the 'value' is only the default value in case the form
1477 # has not yet been filled in. In the complain phase we must
1478 # ignore 'value' but reuse the input from the HTML variables -
1479 # even if they are not syntactically correct. Calling from_html_vars
1480 # here is *not* an option since this might not work in case of
1481 # a wrong user input.
1483 # Special styling for filters
1484 extra_css = "filter" if self._delete_style == "filter" else None
1486 # In the 'complain' phase, where the user already saved the
1487 # form but the validation failed, we must not display the
1488 # original 'value' but take the value from the HTML variables.
1489 if html.request.var("%s_active" % varprefix):
1490 value = self.from_html_vars(varprefix)
1492 # Save all selected items
1493 html.hidden_field(
1494 '%s_active' % varprefix,
1495 ';'.join([k for k in value.keys() if k in self._choice_dict]),
1496 id_='%s_active' % varprefix,
1497 add_var=True)
1499 # Actual table of currently existing entries
1500 html.open_table(id_="%s_table" % varprefix, class_=["valuespec_listof", extra_css])
1502 def render_content():
1503 html.open_td(class_=["vlof_content", extra_css])
1504 vs.render_input(prefix, value.get(ident))
1505 html.close_td()
1507 def render_del():
1508 html.open_td(class_=["vlof_buttons", extra_css])
1509 self.del_button(varprefix, ident)
1510 html.close_td()
1512 for ident, vs in self._choices:
1513 cls = 'unused' if ident not in value else ''
1514 prefix = varprefix + '_' + ident
1515 html.open_tr(id_="%s_row" % prefix, class_=cls)
1516 if self._delete_style == "filter":
1517 render_content()
1518 render_del()
1519 else:
1520 render_del()
1521 render_content()
1522 html.close_tr()
1523 html.close_table()
1524 html.br()
1526 choices = [('', '')] + [(ident, vs.title()) for ident, vs in self._choices]
1527 html.dropdown(
1528 varprefix + '_choice',
1529 choices,
1530 style="width: %dex" % self._size if self._size is not None else None,
1531 class_="vlof_filter" if self._delete_style == "filter" else None)
1532 html.javascript('cmk.valuespecs.listofmultiple_init(\'%s\');' % varprefix)
1533 html.jsbutton(varprefix + '_add', self._add_label,
1534 "cmk.valuespecs.listofmultiple_add('%s')" % varprefix)
1536 def canonical_value(self):
1537 return {}
1539 def value_to_text(self, value):
1540 table_content = HTML()
1541 for ident, val in value:
1542 vs = self._choice_dict[ident]
1543 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
1544 table_content += html.render_tr(html.render_td(vs.title())\
1545 + html.render_td( HTML(vs.value_to_text(val)) ))
1546 return "%s" % html.render_table(table_content)
1548 def from_html_vars(self, varprefix):
1549 value = {}
1550 active = html.request.var('%s_active' % varprefix).strip()
1551 if not active:
1552 return value
1554 for ident in active.split(';'):
1555 vs = self._choice_dict[ident]
1556 value[ident] = vs.from_html_vars(varprefix + '_' + ident)
1557 return value
1559 def validate_datatype(self, value, varprefix):
1560 if not isinstance(value, dict):
1561 raise MKUserError(varprefix, _("The type must be dict, but is %s") % _type_name(value))
1562 for ident, val in value.items():
1563 self._choice_dict[ident].validate_datatype(val, varprefix + '_' + ident)
1565 def validate_value(self, value, varprefix):
1566 for ident, val in value.items():
1567 self._choice_dict[ident].validate_value(val, varprefix + '_' + ident)
1568 ValueSpec.custom_validate(self, value, varprefix)
1571 # Same but for floating point values
1572 class Float(Integer):
1573 def __init__(self, **kwargs):
1574 Integer.__init__(self, **kwargs)
1575 self._decimal_separator = kwargs.get("decimal_separator", ".")
1576 self._display_format = kwargs.get("display_format", "%.2f")
1577 self._allow_int = kwargs.get("allow_int", False)
1579 def render_input(self, varprefix, value):
1580 self.classtype_info()
1581 Integer.render_input(self, varprefix, value)
1583 def _render_value(self, value):
1584 return self._display_format % utils.savefloat(value)
1586 def canonical_value(self):
1587 return float(Integer.canonical_value(self))
1589 def value_to_text(self, value):
1590 return Integer.value_to_text(self, value).replace(".", self._decimal_separator)
1592 def from_html_vars(self, varprefix):
1593 try:
1594 return float(html.request.var(varprefix))
1595 except:
1596 raise MKUserError(
1597 varprefix,
1598 _("The text <b><tt>%s</tt></b> is not a valid floating point number.") %
1599 html.request.var(varprefix))
1601 def validate_datatype(self, value, varprefix):
1602 if isinstance(value, float):
1603 return
1605 if isinstance(value, numbers.Integral) and self._allow_int:
1606 return
1608 raise MKUserError(
1609 varprefix,
1610 _("The value %r has type %s, but must be of type float%s") %
1611 (value, _type_name(value), _(" or int") if self._allow_int else ''))
1614 class Percentage(Float):
1615 def __init__(self, **kwargs):
1616 Float.__init__(self, **kwargs)
1617 if "minvalue" not in kwargs:
1618 self._minvalue = 0.0
1619 if "maxvalue" not in kwargs:
1620 self._maxvalue = 101.0
1621 if "unit" not in kwargs:
1622 self._unit = "%"
1623 if "display_format" not in kwargs:
1624 self._display_format = "%.1f"
1626 self._allow_int = kwargs.get("allow_int", False)
1628 def value_to_text(self, value):
1629 return (self._display_format + "%%") % value
1631 def validate_datatype(self, value, varprefix):
1632 if self._allow_int:
1633 if not isinstance(value, (int, float)):
1634 raise MKUserError(
1635 varprefix,
1636 _("The value %r has type %s, but must be either float or int") %
1637 (value, _type_name(value)))
1638 else:
1639 Float.validate_datatype(self, value, varprefix)
1642 class Checkbox(ValueSpec):
1643 def __init__(self, **kwargs):
1644 ValueSpec.__init__(self, **kwargs)
1645 self._label = kwargs.get("label")
1646 self._true_label = kwargs.get("true_label", _("on"))
1647 self._false_label = kwargs.get("false_label", _("off"))
1648 self._onclick = kwargs.get("onclick")
1650 def canonical_value(self):
1651 return False
1653 def render_input(self, varprefix, value):
1654 self.classtype_info()
1655 html.checkbox(varprefix, value, label=self._label, onclick=self._onclick)
1657 def value_to_text(self, value):
1658 return self._true_label if value else self._false_label
1660 def from_html_vars(self, varprefix):
1661 return bool(html.request.var(varprefix))
1663 def validate_datatype(self, value, varprefix):
1664 if not isinstance(value, bool):
1665 raise MKUserError(
1666 varprefix,
1667 _("The value %r has type %s, but must be of type bool") % (value,
1668 _type_name(value)))
1671 # A type-save dropdown choice. Parameters:
1672 # help_separator: if you set this to a character, e.g. "-", then
1673 # value_to_texg will omit texts from the character up to the end of
1674 # a choices name.
1675 # Note: The list of choices may contain 2-tuples or 3-tuples.
1676 # The format is (value, text {, icon} )
1677 # choices may also be a function that returns - when called
1678 # wihtout arguments - such a tuple list. That way the choices
1679 # can by dynamically computed
1680 class DropdownChoice(ValueSpec):
1681 def __init__(self, **kwargs):
1682 super(DropdownChoice, self).__init__(**kwargs)
1683 self._choices = kwargs["choices"]
1684 self._help_separator = kwargs.get("help_separator")
1685 self._label = kwargs.get("label")
1686 self._prefix_values = kwargs.get("prefix_values", False)
1687 self._sorted = kwargs.get("sorted", False)
1688 self._empty_text = kwargs.get("empty_text",
1689 _("There are no elements defined for this selection yet."))
1690 self._invalid_choice = kwargs.get("invalid_choice", "complain") # also possible: "replace"
1691 self._invalid_choice_title = kwargs.get("invalid_choice_title",
1692 _("Element '%r' does not exist anymore"))
1693 self._invalid_choice_error = kwargs.get(
1694 "invalid_choice_error",
1695 _("The selected element is not longer available. Please select something else."))
1696 self._no_preselect = kwargs.get("no_preselect", False)
1697 self._no_preselect_value = kwargs.get("no_preselect_value", None)
1698 self._no_preselect_title = kwargs.get("no_preselect_title", "") # if not preselected
1699 self._no_preselect_error = kwargs.get("no_preselect_error", _("Please make a selection"))
1700 self._on_change = kwargs.get("on_change")
1701 self._read_only = kwargs.get("read_only", False)
1702 self._encode_value = kwargs.get("encode_value", True)
1704 def choices(self):
1705 result = []
1706 if isinstance(self._choices, list):
1707 result = self._choices
1708 elif isinstance(self._choices, dict):
1709 result = ListChoice.dict_choices(self._choices)
1710 else:
1711 result = self._choices()
1713 if self._no_preselect:
1714 return [(self._no_preselect_value, self._no_preselect_title)] + result
1715 return result
1717 def canonical_value(self):
1718 choices = self.choices()
1719 if len(choices) > 0:
1720 return choices[0][0]
1721 return None
1723 def render_input(self, varprefix, value):
1724 self.classtype_info()
1725 if self._label:
1726 html.write("%s " % self._label)
1728 choices = self.choices()
1730 defval = choices[0][0] if choices else None
1731 options = []
1732 for entry in self.choices():
1733 if self._prefix_values:
1734 entry = (entry[0], "%s - %s" % entry)
1736 options.append(entry)
1737 if entry[0] == value:
1738 defval = entry[0]
1740 # In complain mode: Use the value received from the HTML variable
1741 if self._invalid_choice == "complain" and value is not None and self._value_is_invalid(
1742 value):
1743 defval = value
1744 options.append((defval, self._get_invalid_choice_title(value)))
1746 if value is None and not options:
1747 html.write(self._empty_text)
1748 return
1750 if len(options) == 0:
1751 html.write(self._empty_text)
1752 elif len(options[0]) == 3:
1753 html.icon_dropdown(
1754 varprefix, self._options_for_html(options), deflt=self._option_for_html(defval))
1755 else:
1756 html.dropdown(
1757 varprefix,
1758 self._options_for_html(options),
1759 deflt=self._option_for_html(defval),
1760 onchange=self._on_change,
1761 ordered=self._sorted,
1762 read_only=self._read_only)
1764 def _get_invalid_choice_title(self, value):
1765 if "%s" in self._invalid_choice_title or "%r" in self._invalid_choice_title:
1766 return self._invalid_choice_title % (value,)
1767 return self._invalid_choice_title
1769 def value_to_text(self, value):
1770 for entry in self.choices():
1771 val, title = entry[:2]
1772 if value == val:
1773 if self._help_separator:
1774 return html.attrencode(title.split(self._help_separator, 1)[0].strip())
1775 return html.attrencode(title)
1776 return html.attrencode(self._get_invalid_choice_title(value))
1778 def from_html_vars(self, varprefix):
1779 choices = self.choices()
1781 for entry in choices:
1782 val, _title = entry[:2]
1783 if self._is_selected_option_from_html(varprefix, val):
1784 return val
1786 if self._invalid_choice == "replace":
1787 return self.default_value() # garbled URL or len(choices) == 0
1788 elif not choices:
1789 raise MKUserError(varprefix, self._empty_text)
1790 else:
1791 raise MKUserError(varprefix, self._invalid_choice_error)
1793 def _is_selected_option_from_html(self, varprefix, val):
1794 selected_value = html.request.var(varprefix)
1795 return selected_value == self._option_for_html(val)
1797 def _option_for_html(self, value):
1798 if self._encode_value:
1799 return self.option_id(value)
1800 return value
1802 def _options_for_html(self, orig_options):
1803 options = []
1804 for val, title in orig_options:
1805 options.append((self._option_for_html(val), title))
1806 return options
1808 @staticmethod
1809 def option_id(val):
1810 return "%s" % hashlib.sha256(repr(val)).hexdigest()
1812 def validate_value(self, value, varprefix):
1813 if self._no_preselect and value == self._no_preselect_value:
1814 raise MKUserError(varprefix, self._no_preselect_error)
1816 if self._invalid_choice == "complain" and self._value_is_invalid(value):
1817 if value is not None:
1818 raise MKUserError(varprefix, self._invalid_choice_error)
1819 else:
1820 raise MKUserError(varprefix, self._empty_text)
1822 ValueSpec.custom_validate(self, value, varprefix)
1824 def _value_is_invalid(self, value):
1825 for entry in self.choices():
1826 if entry[0] == value:
1827 return False
1828 return True
1831 # Special conveniance variant for monitoring states
1832 # TODO: Rename to ServiceState() or something like this
1833 class MonitoringState(DropdownChoice):
1834 def __init__(self, **kwargs):
1835 choices = [
1836 (0, _("OK")),
1837 (1, _("WARN")),
1838 (2, _("CRIT")),
1839 (3, _("UNKNOWN")),
1841 kwargs.setdefault("default_value", 0)
1842 DropdownChoice.__init__(self, choices=choices, **kwargs)
1845 class HostState(DropdownChoice):
1846 def __init__(self, **kwargs):
1847 choices = [
1848 (0, _("UP")),
1849 (1, _("DOWN")),
1850 (2, _("UNREACHABLE")),
1852 kwargs.setdefault("default_value", 0)
1853 DropdownChoice.__init__(self, choices=choices, **kwargs)
1856 # A Dropdown choice where the elements are ValueSpecs.
1857 # The currently selected ValueSpec will be displayed.
1858 # The text representations of the ValueSpecs will be used as texts.
1859 # A ValueSpec of None is also allowed and will return
1860 # the value None. It is also allowed to leave out the
1861 # value spec for some of the choices (which is the same as
1862 # using None).
1863 # The resulting value is either a single value (if no
1864 # value spec is defined for the selected entry) or a pair
1865 # of (x, y) where x is the value of the selected entry and
1866 # y is the value of the valuespec assigned to that entry.
1867 # choices is a list of triples: [ ( value, title, vs ), ... ]
1868 class CascadingDropdown(ValueSpec):
1869 class Render(Enum):
1870 normal = "normal"
1871 foldable = "foldable"
1873 def __init__(self, **kwargs):
1874 ValueSpec.__init__(self, **kwargs)
1876 if isinstance(kwargs["choices"], list):
1877 self._choices = self.normalize_choices(kwargs["choices"])
1878 else:
1879 self._choices = kwargs["choices"] # function, store for later
1881 self._label = kwargs.get("label")
1882 self._separator = kwargs.get("separator", ", ")
1883 self._sorted = kwargs.get("sorted", True)
1884 self._orientation = kwargs.get("orientation", "vertical") # or horizontal
1885 self._render = kwargs.get("render", CascadingDropdown.Render.normal)
1886 if kwargs.get("encoding", "tuple") == "list":
1887 self._encoding_type = list
1888 else:
1889 self._encoding_type = tuple
1891 self._no_elements_text = kwargs.get("no_elements_text",
1892 _("There are no elements defined for this selection"))
1894 self._no_preselect = kwargs.get("no_preselect", False)
1895 self._no_preselect_value = kwargs.get("no_preselect_value", None)
1896 self._no_preselect_title = kwargs.get("no_preselect_title", "") # if not preselected
1897 self._no_preselect_error = kwargs.get("no_preselect_error", _("Please make a selection"))
1899 def normalize_choices(self, choices):
1900 new_choices = []
1901 for entry in choices:
1902 if len(entry) == 2: # plain entry with no sub-valuespec
1903 entry = entry + (None,) # normlize to three entries
1904 new_choices.append(entry)
1905 return new_choices
1907 def choices(self):
1908 if isinstance(self._choices, list):
1909 result = self._choices
1910 else:
1911 result = self.normalize_choices(self._choices())
1913 if self._no_preselect:
1914 result = [(self._no_preselect_value, self._no_preselect_title, None)] \
1915 + result
1917 return result
1919 def canonical_value(self):
1920 choices = self.choices()
1921 if not choices:
1922 return None
1924 if choices[0][2]:
1925 return self._encoding_type((choices[0][0], choices[0][2].canonical_value()))
1926 return choices[0][0]
1928 def default_value(self):
1929 try:
1930 return self._default_value
1931 except:
1932 choices = self.choices()
1933 if not choices:
1934 return None
1936 if choices[0][2]:
1937 return self._encoding_type((choices[0][0], choices[0][2].default_value()))
1938 return choices[0][0]
1940 def render_input(self, varprefix, value):
1941 self.classtype_info()
1942 def_val = '0'
1943 options = []
1944 choices = self.choices()
1945 if not choices:
1946 html.write(self._no_elements_text)
1947 return
1949 for nr, (val, title, vs) in enumerate(choices):
1950 options.append((str(nr), title))
1951 # Determine the default value for the select, so the
1952 # the dropdown pre-selects the line corresponding with value.
1953 # Note: the html.dropdown() with automatically show the modified
1954 # selection, if the HTML variable varprefix_sel aleady
1955 # exists.
1956 if value == val or (isinstance(value, self._encoding_type) and value[0] == val):
1957 def_val = str(nr)
1959 vp = varprefix + "_sel"
1960 onchange = "cmk.valuespecs.cascading_change(this, '%s', %d);" % (varprefix, len(choices))
1961 html.dropdown(
1962 vp, options, deflt=def_val, onchange=onchange, ordered=self._sorted, label=self._label)
1964 # make sure, that the visibility is done correctly, in both
1965 # cases:
1966 # 1. Form painted for the first time (no submission yet, vp missing in URL)
1967 # 2. Form already submitted -> honor URL variable vp for visibility
1968 cur_val = html.request.var(vp)
1970 if self._orientation == "vertical":
1971 html.br()
1972 else:
1973 html.nbsp()
1974 for nr, (val, title, vs) in enumerate(choices):
1975 if vs:
1976 vp = varprefix + "_%d" % nr
1977 # Form already submitted once (and probably in complain state)
1978 if cur_val is not None:
1979 try:
1980 def_val_2 = vs.from_html_vars(vp)
1981 except MKUserError:
1982 def_val_2 = vs.default_value()
1983 if cur_val == str(nr):
1984 disp = ""
1985 else:
1986 disp = "none"
1987 else: # form painted the first time
1988 if value == val \
1989 or (isinstance(value, self._encoding_type) and value[0] == val):
1990 if isinstance(value, self._encoding_type):
1991 def_val_2 = value[1]
1992 else:
1993 def_val_2 = vs.default_value()
1994 disp = ""
1995 else:
1996 def_val_2 = vs.default_value()
1997 disp = "none"
1998 html.open_span(id_="%s_%s_sub" % (varprefix, nr), style="display:%s;" % disp)
1999 html.help(vs.help())
2000 vs.render_input(vp, def_val_2)
2001 html.close_span()
2003 def value_to_text(self, value):
2004 choices = self.choices()
2005 for val, title, vs in choices:
2006 if (vs and value and value[0] == val) or \
2007 (value == val):
2008 if not vs:
2009 return title
2011 rendered_value = vs.value_to_text(value[1])
2012 if not rendered_value:
2013 return title
2015 if self._render == CascadingDropdown.Render.foldable:
2016 with html.plugged():
2017 html.begin_foldable_container(
2018 "foldable_cascading_dropdown",
2019 id_=hashlib.sha256(repr(value)).hexdigest(),
2020 isopen=False,
2021 title=title,
2022 indent=False)
2023 html.write(vs.value_to_text(value[1]))
2024 html.end_foldable_container()
2025 return html.drain()
2027 return title + self._separator + \
2028 vs.value_to_text(value[1])
2030 return "" # Nothing selected? Should never happen
2032 def from_html_vars(self, varprefix):
2033 choices = self.choices()
2035 # No choices and "no elements text" is shown: The html var is
2036 # not present and no choice can be made. So fallback to default
2037 # value and let the validation methods lead to an error message.
2038 if not choices:
2039 return self.default_value()
2041 try:
2042 sel = int(html.request.var(varprefix + "_sel"))
2043 except:
2044 sel = 0
2045 val, _title, vs = choices[sel]
2046 if vs:
2047 val = self._encoding_type((val, vs.from_html_vars(varprefix + "_%d" % sel)))
2048 return val
2050 def validate_datatype(self, value, varprefix):
2051 choices = self.choices()
2052 for nr, (val, _title, vs) in enumerate(choices):
2053 if value == val or (isinstance(value, self._encoding_type) and value[0] == val):
2054 if vs:
2055 if not isinstance(value, self._encoding_type) or len(value) != 2:
2056 raise MKUserError(
2057 varprefix + "_sel",
2058 _("Value must be a %s with two elements.") %
2059 self._encoding_type.__name__)
2060 vs.validate_datatype(value[1], varprefix + "_%d" % nr)
2061 return
2062 raise MKUserError(varprefix + "_sel", _("Value %r is not allowed here.") % value)
2064 def validate_value(self, value, varprefix):
2065 if self._no_preselect and value == self._no_preselect_value:
2066 raise MKUserError(varprefix + "_sel", self._no_preselect_error)
2068 choices = self.choices()
2069 for nr, (val, _title, vs) in enumerate(choices):
2070 if value == val or (isinstance(value, self._encoding_type) and value[0] == val):
2071 if vs:
2072 vs.validate_value(value[1], varprefix + "_%d" % nr)
2073 ValueSpec.custom_validate(self, value, varprefix)
2074 return
2075 raise MKUserError(varprefix + "_sel", _("Value %r is not allowed here.") % (value,))
2078 # The same logic as the dropdown choice, but rendered
2079 # as a group of radio buttons.
2080 # columns is None or unset -> separate with "&nbsp;"
2081 class RadioChoice(DropdownChoice):
2082 def __init__(self, **kwargs):
2083 DropdownChoice.__init__(self, **kwargs)
2084 self._columns = kwargs.get("columns")
2085 # Allow orientation as corner cases of columns
2086 orientation = kwargs.get("orientation")
2087 if orientation == "vertical":
2088 self._columns = 1
2089 elif orientation == "horizontal":
2090 self._columns = 9999999
2092 def render_input(self, varprefix, value):
2093 self.classtype_info()
2094 html.begin_radio_group()
2095 if self._columns is not None:
2096 html.open_table(class_=["radiochoice"])
2097 html.open_tr()
2099 if self._sorted:
2100 choices = self._choices[:]
2101 choices.sort(cmp=lambda a, b: cmp(a[1], b[1]))
2102 else:
2103 choices = self._choices
2105 for index, entry in enumerate(choices):
2106 if self._columns is not None:
2107 html.open_td()
2109 if len(entry) > 2 and entry[2] is not None: # icon!
2110 label = html.render_icon(entry[2], entry[1])
2111 else:
2112 label = entry[1]
2114 html.radiobutton(varprefix, self.option_id(entry[0]), value == entry[0], label)
2116 if len(entry) > 3 and entry[3]:
2117 html.open_p()
2118 html.write(entry[3])
2119 html.close_p()
2121 if self._columns is not None:
2122 html.close_td()
2123 if (index + 1) % self._columns == 0 and (index + 1) < len(self._choices):
2124 html.tr('')
2125 else:
2126 html.nbsp()
2128 if self._columns is not None:
2129 mod = len(self._choices) % self._columns
2130 if mod:
2131 for _td_counter in range(self._columns - mod - 1):
2132 html.td('')
2133 html.close_tr()
2134 html.close_table()
2136 html.end_radio_group()
2139 # A list of checkboxes representing a list of values
2140 class ListChoice(ValueSpec):
2141 @staticmethod
2142 def dict_choices(choices):
2143 return [("%s" % type_id, "%d - %s" % (type_id, type_name))
2144 for (type_id, type_name) in sorted(choices.items())]
2146 def __init__(self, **kwargs):
2147 ValueSpec.__init__(self, **kwargs)
2148 self._choices = kwargs.get("choices")
2149 self._columns = kwargs.get("columns", 1)
2150 self._allow_empty = kwargs.get("allow_empty", True)
2151 self._empty_text = kwargs.get("empty_text", _("(nothing selected)"))
2152 self._loaded_at = None
2153 self._render_function = kwargs.get("render_function", lambda id, val: val)
2154 self._toggle_all = kwargs.get("toggle_all", False)
2155 self._render_orientation = kwargs.get("render_orientation", "horizontal") # other: vertical
2156 self._no_elements_text = kwargs.get("no_elements_text",
2157 _("There are no elements defined for this selection"))
2159 # In case of overloaded functions with dynamic elements
2160 def load_elements(self):
2161 if self._choices is not None:
2162 if isinstance(self._choices, list):
2163 self._elements = self._choices
2164 elif isinstance(self._choices, dict):
2165 self._elements = ListChoice.dict_choices(self._choices)
2166 else:
2167 self._elements = self._choices()
2168 return
2170 if self._loaded_at != id(html):
2171 self._elements = self.get_elements()
2172 self._loaded_at = id(html) # unique for each query!
2174 def get_elements(self):
2175 raise NotImplementedError()
2177 def canonical_value(self):
2178 return []
2180 def _draw_listchoice(self, varprefix, value, elements, columns, toggle_all):
2182 if self._toggle_all:
2183 html.a(
2184 _("Check / Uncheck all"),
2185 href="javascript:cmk.valuespecs.list_choice_toggle_all('%s')" % varprefix)
2187 html.open_table(id_="%s_tbl" % varprefix, class_=["listchoice"])
2188 for nr, (key, title) in enumerate(elements):
2189 if nr % self._columns == 0:
2190 if nr > 0:
2191 html.close_tr()
2192 html.open_tr()
2193 html.open_td()
2194 html.checkbox("%s_%d" % (varprefix, nr), key in value, label=title)
2195 html.close_td()
2196 html.close_tr()
2197 html.close_table()
2199 def render_input(self, varprefix, value):
2200 self.classtype_info()
2201 self.load_elements()
2202 if not self._elements:
2203 html.write(self._no_elements_text)
2204 return
2206 self._draw_listchoice(varprefix, value, self._elements, self._columns, self._toggle_all)
2208 # Make sure that at least one variable with the prefix is present
2209 html.hidden_field(varprefix, "1", add_var=True)
2211 def value_to_text(self, value):
2212 if not value:
2213 return self._empty_text
2215 self.load_elements()
2216 d = dict(self._elements)
2217 texts = [self._render_function(v, d.get(v, v)) for v in value]
2218 if self._render_orientation == "horizontal":
2219 return ", ".join(texts)
2221 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
2222 return "%s" % html.render_table(
2223 html.render_tr(html.render_td(html.render_br().join(HTML(x) for x in texts))))
2224 #OLD: return "<table><tr><td>" + "<br>".join(texts) + "</td></tr></table>"
2226 def from_html_vars(self, varprefix):
2227 self.load_elements()
2228 value = []
2230 for nr, (key, _title) in enumerate(self._elements):
2231 if html.get_checkbox("%s_%d" % (varprefix, nr)):
2232 value.append(key)
2233 return value
2235 def validate_datatype(self, value, varprefix):
2236 self.load_elements()
2238 if not isinstance(value, list):
2239 raise MKUserError(varprefix,
2240 _("The datatype must be list, but is %s") % _type_name(value))
2242 for v in value:
2243 if self._value_is_invalid(v):
2244 raise MKUserError(varprefix, _("%s is not an allowed value") % v)
2246 def validate_value(self, value, varprefix):
2247 if not self._allow_empty and not value:
2248 raise MKUserError(varprefix, _('You have to select at least one element.'))
2249 ValueSpec.custom_validate(self, value, varprefix)
2251 def _value_is_invalid(self, value):
2252 d = dict(self._elements)
2253 return value not in d
2256 # Implements a choice of items which is realized with
2257 # two ListChoices select fields. One contains all available
2258 # items and one contains all selected items.
2259 # Optionally you can have the user influance the order of
2260 # the entries by simply clicking them in a certain order.
2261 # If that feature is not being used, then the original order
2262 # of the elements is always being kept.
2263 # TODO: Beware: the keys in this choice are not type safe.
2264 # They can only be strings. They must not contain | or other
2265 # dangerous characters. We should fix this and make it this
2266 # compatible to DropdownChoice()
2267 class DualListChoice(ListChoice):
2268 def __init__(self, **kwargs):
2269 super(DualListChoice, self).__init__(**kwargs)
2270 self._autoheight = kwargs.get("autoheight", False)
2271 self._custom_order = kwargs.get("custom_order", False)
2272 self._instant_add = kwargs.get("instant_add", False)
2273 self._enlarge_active = kwargs.get("enlarge_active", False)
2274 if "rows" in kwargs:
2275 self._rows = kwargs.get("rows", 5)
2276 self._autoheight = False
2277 else:
2278 self._rows = 5
2279 self._size = kwargs.get("size") # Total width in ex
2281 def render_input(self, varprefix, value):
2282 self.classtype_info()
2284 self.load_elements()
2285 if not self._elements:
2286 html.write_text(_("There are no elements for selection."))
2287 return
2289 # Use values from HTTP request in complain mode
2290 if value is None:
2291 value = self.from_html_vars(varprefix)
2293 selected = []
2294 unselected = []
2295 if self._custom_order:
2296 edict = dict(self._elements)
2297 allowed_keys = edict.keys()
2298 for v in value:
2299 if v in allowed_keys:
2300 selected.append((v, edict[v]))
2302 for v, _name in self._elements:
2303 if v not in value:
2304 unselected.append((v, edict[v]))
2305 else:
2306 for e in self._elements:
2307 if e[0] in value:
2308 selected.append(e)
2309 else:
2310 unselected.append(e)
2312 select_func = 'cmk.valuespecs.duallist_switch(\'unselected\', \'%s\', %d);' % (
2313 varprefix, 1 if self._custom_order else 0)
2314 unselect_func = 'cmk.valuespecs.duallist_switch(\'selected\', \'%s\', %d);' % (
2315 varprefix, 1 if self._custom_order else 0)
2317 html.open_table(
2318 class_=["vs_duallist"],
2319 style="width: %dpx;" % (self._size * 6.4) if self._size else None)
2321 html.open_tr()
2322 html.open_td(class_="head")
2323 html.write_text(_('Available'))
2324 if not self._instant_add:
2325 html.a(">", href="javascript:%s;" % select_func, class_=["control", "add"])
2326 html.close_td()
2328 html.open_td(class_="head")
2329 html.write_text(_('Selected'))
2330 if not self._instant_add:
2331 html.a("<", href="javascript:%s;" % unselect_func, class_=["control", "del"])
2332 html.close_td()
2333 html.close_tr()
2335 html.open_tr()
2336 for suffix, choices, select_func in [
2337 ("unselected", unselected, select_func),
2338 ("selected", selected, unselect_func),
2341 onchange_func = select_func if self._instant_add else ''
2342 if self._enlarge_active:
2343 onchange_func = 'cmk.valuespecs.duallist_enlarge(%s, %s);' % (json.dumps(suffix),
2344 json.dumps(varprefix))
2346 attrs = {
2347 'multiple': 'multiple',
2348 'style': 'height:auto' if self._autoheight else "height: %dpx" % (self._rows * 16),
2349 'ondblclick': select_func if not self._instant_add else '',
2352 html.open_td()
2353 attrs["onchange"] = onchange_func
2354 html.multi_select(
2355 "%s_%s" % (varprefix, suffix),
2356 choices,
2357 deflt='',
2358 ordered=self._custom_order,
2359 **attrs)
2360 html.close_td()
2361 html.close_tr()
2363 html.close_table()
2364 html.hidden_field(
2365 varprefix, '|'.join([k for k, v in selected]), id_=varprefix, add_var=True)
2367 def validate_value(self, value, varprefix):
2368 try:
2369 ListChoice.validate_value(self, value, varprefix)
2370 except MKUserError as e:
2371 raise MKUserError(e.varname + "_selected", e.message)
2373 def from_html_vars(self, varprefix):
2374 self.load_elements()
2375 selected = html.request.var(varprefix, '').split('|')
2376 value = []
2377 if self._custom_order:
2378 edict = dict(self._elements)
2379 allowed_keys = edict.keys()
2380 for v in selected:
2381 if v in allowed_keys:
2382 value.append(v)
2383 else:
2384 for key, _title in self._elements:
2385 if key in selected:
2386 value.append(key)
2387 return value
2390 # A type-save dropdown choice with one extra field that
2391 # opens a further value spec for entering an alternative
2392 # Value.
2393 class OptionalDropdownChoice(DropdownChoice):
2394 def __init__(self, **kwargs):
2395 DropdownChoice.__init__(self, **kwargs)
2396 self._explicit = kwargs["explicit"]
2397 self._otherlabel = kwargs.get("otherlabel", _("Other"))
2399 def canonical_value(self):
2400 return self._explicit.canonical_value()
2402 def value_is_explicit(self, value):
2403 return value not in [c[0] for c in self.choices()]
2405 def render_input(self, varprefix, value):
2406 self.classtype_info()
2407 defval = "other"
2408 options = []
2409 for n, (val, title) in enumerate(self.choices()):
2410 options.append((str(n), title))
2411 if val == value:
2412 defval = str(n)
2413 if self._sorted:
2414 options.sort(cmp=lambda a, b: cmp(a[1], b[1]))
2415 options.append(("other", self._otherlabel))
2416 html.dropdown(
2417 varprefix,
2418 options,
2419 deflt=defval, # style="float:left;",
2420 onchange="cmk.valuespecs.toggle_dropdown(this, '%s_ex');" % varprefix)
2421 if html.request.has_var(varprefix):
2422 div_is_open = html.request.var(varprefix) == "other"
2423 else:
2424 div_is_open = self.value_is_explicit(value)
2426 html.open_span(
2427 id_="%s_ex" % varprefix,
2428 style=["white-space: nowrap;", None if div_is_open else "display:none;"])
2429 html.nbsp()
2431 if defval == "other":
2432 input_value = value
2433 else:
2434 input_value = self._explicit.default_value()
2435 html.help(self._explicit.help())
2436 self._explicit.render_input(varprefix + "_ex", input_value)
2437 html.close_span()
2439 def value_to_text(self, value):
2440 for val, title in self.choices():
2441 if val == value:
2442 return title
2443 return self._explicit.value_to_text(value)
2445 def from_html_vars(self, varprefix):
2446 choices = self.choices()
2447 sel = html.request.var(varprefix)
2448 if sel == "other":
2449 return self._explicit.from_html_vars(varprefix + "_ex")
2451 for n, (val, _title) in enumerate(choices):
2452 if sel == str(n):
2453 return val
2454 return choices[0][0] # can only happen if user garbled URL
2456 def validate_value(self, value, varprefix):
2457 if self.value_is_explicit(value):
2458 self._explicit.validate_value(value, varprefix)
2459 # else valid_datatype already has made the job
2460 ValueSpec.custom_validate(self, value, varprefix)
2462 def validate_datatype(self, value, varprefix):
2463 for val, _title in self.choices():
2464 if val == value:
2465 return
2466 self._explicit.validate_datatype(value, varprefix + "_ex")
2469 # Input of date with optimization for nearby dates
2470 # in the future. Useful for example for alarms. The
2471 # date is represented by a UNIX timestamp where the
2472 # seconds are silently ignored.
2473 def round_date(t):
2474 return int(t) / seconds_per_day * seconds_per_day
2477 def today():
2478 return round_date(time.time())
2481 class Weekday(DropdownChoice):
2482 def __init__(self, **kwargs):
2483 kwargs['choices'] = sorted(defines.weekdays().items())
2484 DropdownChoice.__init__(self, **kwargs)
2487 class RelativeDate(OptionalDropdownChoice):
2488 def __init__(self, **kwargs):
2489 choices = [
2490 (0, _("today")),
2491 (1, _("tomorrow")),
2493 weekday = time.localtime(today()).tm_wday
2494 for w in range(2, 7):
2495 wd = (weekday + w) % 7
2496 choices.append((w, defines.weekday_name(wd)))
2497 for w in range(0, 7):
2498 wd = (weekday + w) % 7
2499 if w < 2:
2500 title = _(" next week")
2501 else:
2502 title = _(" in %d days") % (w + 7)
2503 choices.append((w + 7, defines.weekday_name(wd) + title))
2505 kwargs['choices'] = choices
2506 kwargs['explicit'] = Integer()
2507 kwargs['otherlabel'] = _("in ... days")
2509 OptionalDropdownChoice.__init__(self, **kwargs)
2511 if "default_days" in kwargs:
2512 self._default_value = kwargs["default_days"] * seconds_per_day + today()
2513 else:
2514 self._default_value = today()
2516 def canonical_value(self):
2517 return self._default_value
2519 def render_input(self, varprefix, value):
2520 self.classtype_info()
2521 reldays = (round_date(value) - today()) / seconds_per_day
2522 OptionalDropdownChoice.render_input(self, varprefix, reldays)
2524 def value_to_text(self, value):
2525 reldays = (round_date(value) - today()) / seconds_per_day
2526 if reldays == -1:
2527 return _("yesterday")
2528 elif reldays == -2:
2529 return _("two days ago")
2530 elif reldays < 0:
2531 return _("%d days ago") % -reldays
2532 elif reldays < len(self._choices):
2533 return self._choices[reldays][1]
2534 return _("in %d days") % reldays
2536 def from_html_vars(self, varprefix):
2537 reldays = OptionalDropdownChoice.from_html_vars(self, varprefix)
2538 return today() + reldays * seconds_per_day
2540 def validate_datatype(self, value, varprefix):
2541 if not isinstance(value, (int, float)):
2542 raise MKUserError(varprefix, _("Date must be a number value"))
2544 def validate_value(self, value, varprefix):
2545 ValueSpec.custom_validate(self, value, varprefix)
2548 # A ValueSpec for editing a date. The date is
2549 # represented as a UNIX timestamp x where x % seconds_per_day
2550 # is zero (or will be ignored if non-zero), as long as
2551 # include_time is not set to True
2552 class AbsoluteDate(ValueSpec):
2553 def __init__(self, **kwargs):
2554 ValueSpec.__init__(self, **kwargs)
2555 self._show_titles = kwargs.get("show_titles", True)
2556 self._label = kwargs.get("label")
2557 self._include_time = kwargs.get("include_time", False)
2558 self._format = kwargs.get("format", "%F %T" if self._include_time else "%F")
2559 self._default_value = kwargs.get("default_value", None)
2560 self._allow_empty = kwargs.get('allow_empty', False)
2561 # The default is that "None" means show current date/time in the
2562 # input fields. This option changes the input fields to be empty by default
2563 # and makes the value able to be None when no time is set.
2564 # FIXME: Shouldn't this be the default?
2565 self._none_means_empty = kwargs.get("none_means_empty", False)
2567 def default_value(self):
2568 if self._default_value is not None:
2569 return self._default_value
2571 if self._allow_empty:
2572 return None
2574 if self._include_time:
2575 return time.time()
2576 return today()
2578 def canonical_value(self):
2579 return self.default_value()
2581 def split_date(self, value):
2582 if self._none_means_empty and value is None:
2583 return (None,) * 6
2584 lt = time.localtime(value)
2585 return lt.tm_year, lt.tm_mon, lt.tm_mday, \
2586 lt.tm_hour, lt.tm_min, lt.tm_sec
2588 def render_input(self, varprefix, value):
2590 self.classtype_info()
2592 if self._label:
2593 html.write("%s" % self._label)
2594 html.nbsp()
2596 year, month, day, hour, mmin, sec = self.split_date(value)
2597 values = [
2598 ("_year", year, 4),
2599 ("_month", month, 2),
2600 ("_day", day, 2),
2602 if self._include_time:
2603 values += [
2604 None,
2605 ("_hour", hour, 2),
2606 ("_min", mmin, 2),
2607 ("_sec", sec, 2),
2610 if not self._show_titles:
2611 titles = [_("Year"), _("Month"), _("Day")]
2612 if self._include_time:
2613 titles += ['', _("Hour"), _("Minute"), _("Sec.")]
2615 html.open_table(class_=["vs_date"])
2617 html.open_tr()
2618 map(html.th, titles)
2619 html.close_tr()
2621 html.open_tr()
2622 for val in values:
2623 html.open_td()
2624 if val is None:
2625 html.nbsp()
2626 else:
2627 html.number_input(varprefix + val[0], val[1], size=val[2])
2628 html.close_td()
2629 html.close_tr()
2631 html.close_table()
2633 else:
2634 for count, val in enumerate(values):
2635 if count > 0:
2636 html.write_text(" ")
2637 if val is None:
2638 html.nbsp()
2639 else:
2640 html.number_input(varprefix + val[0], val[1], size=val[2])
2642 def set_focus(self, varprefix):
2643 html.set_focus(varprefix + "_year")
2645 def value_to_text(self, value):
2646 return time.strftime(self._format, time.localtime(value))
2648 def from_html_vars(self, varprefix):
2649 parts = []
2650 entries = [
2651 ("year", _("year"), 1970, 2038),
2652 ("month", _("month"), 1, 12),
2653 ("day", _("day"), 1, 31),
2656 if self._include_time:
2657 entries += [
2658 ("hour", _("hour"), 0, 23),
2659 ("min", _("min"), 0, 59),
2660 ("sec", _("sec"), 0, 59),
2663 for what, title, mmin, mmax in entries:
2664 try:
2665 varname = varprefix + "_" + what
2666 part = int(html.request.var(varname))
2667 except:
2668 if self._allow_empty:
2669 return None
2670 else:
2671 raise MKUserError(varname, _("Please enter a valid number"))
2672 if part < mmin or part > mmax:
2673 raise MKUserError(
2674 varname,
2675 _("The value for %s must be between %d and %d") % (title, mmin, mmax))
2676 parts.append(part)
2678 # Construct broken time from input fields. Assume no-dst
2679 parts += [0] * (3 if self._include_time else 6)
2680 # Convert to epoch
2681 epoch = time.mktime(tuple(parts))
2682 # Convert back to localtime in order to know DST setting
2683 localtime = time.localtime(epoch)
2684 # Enter DST setting of that time
2685 parts[-1] = localtime.tm_isdst
2686 # Convert to epoch again
2687 return time.mktime(tuple(parts))
2689 def validate_datatype(self, value, varprefix):
2690 if value is None and self._allow_empty:
2691 return
2692 if not isinstance(value, (int, float)):
2693 raise MKUserError(
2694 varprefix,
2695 _("The type of the timestamp must be int or float, but is %s") % _type_name(value))
2697 def validate_value(self, value, varprefix):
2698 if (not self._allow_empty and value is None) or value < 0 or int(value) > (2**31 - 1):
2699 return MKUserError(varprefix, _("%s is not a valid UNIX timestamp") % value)
2700 ValueSpec.custom_validate(self, value, varprefix)
2703 # Valuespec for entering times like 00:35 or 16:17. Currently
2704 # no seconds are supported. But this could easily be added.
2705 # The value itself is stored as a pair of integers, a.g.
2706 # (0, 35) or (16, 17). If the user does not enter a time
2707 # the vs will return None.
2708 class Timeofday(ValueSpec):
2709 def __init__(self, **kwargs):
2710 ValueSpec.__init__(self, **kwargs)
2711 self._allow_24_00 = kwargs.get("allow_24_00", False)
2712 self._allow_empty = kwargs.get("allow_empty", True)
2714 def canonical_value(self):
2715 if self._allow_empty:
2716 return None
2717 return (0, 0)
2719 def render_input(self, varprefix, value):
2720 self.classtype_info()
2721 text = ("%02d:%02d" % value) if value else ''
2722 html.text_input(varprefix, text, size=5)
2724 def value_to_text(self, value):
2725 if value is None:
2726 return ""
2727 return "%02d:%02d" % value
2729 def from_html_vars(self, varprefix):
2730 # Fully specified
2731 text = html.request.var(varprefix, "").strip()
2732 if not text:
2733 return None
2735 if re.match("^(24|[0-1][0-9]|2[0-3]):[0-5][0-9]$", text):
2736 return tuple(map(int, text.split(":")))
2738 # only hours
2739 try:
2740 b = int(text)
2741 return (b, 0)
2742 except:
2743 raise MKUserError(
2744 varprefix,
2745 _("Invalid time format '<tt>%s</tt>', please use <tt>24:00</tt> format.") % text)
2747 def validate_datatype(self, value, varprefix):
2748 if self._allow_empty and value is None:
2749 return
2751 if not isinstance(value, tuple):
2752 raise MKUserError(varprefix,
2753 _("The datatype must be tuple, but ist %s") % _type_name(value))
2755 if len(value) != 2:
2756 raise MKUserError(
2757 varprefix,
2758 _("The tuple must contain two elements, but you have %d") % len(value))
2760 for x in value:
2761 if not isinstance(x, int):
2762 raise MKUserError(
2763 varprefix,
2764 _("All elements of the tuple must be of type int, you have %s") % _type_name(x))
2766 def validate_value(self, value, varprefix):
2767 if not self._allow_empty and value is None:
2768 raise MKUserError(varprefix, _("Please enter a time."))
2769 if self._allow_24_00:
2770 max_value = (24, 0)
2771 else:
2772 max_value = (23, 59)
2773 if value > max_value:
2774 raise MKUserError(varprefix,
2775 _("The time must not be greater than %02d:%02d.") % max_value)
2776 elif value[0] < 0 or value[1] < 0 or value[0] > 24 or value[1] > 59:
2777 raise MKUserError(varprefix, _("Hours/Minutes out of range"))
2778 ValueSpec.custom_validate(self, value, varprefix)
2781 # Range like 00:15 - 18:30
2782 class TimeofdayRange(ValueSpec):
2783 def __init__(self, **kwargs):
2784 ValueSpec.__init__(self, **kwargs)
2785 self._allow_empty = kwargs.get("allow_empty", True)
2786 self._bounds = (
2787 Timeofday(allow_empty=self._allow_empty, allow_24_00=True),
2788 Timeofday(allow_empty=self._allow_empty, allow_24_00=True),
2791 def canonical_value(self):
2792 if self._allow_empty:
2793 return None
2794 return (0, 0), (24, 0)
2796 def render_input(self, varprefix, value):
2797 self.classtype_info()
2798 if value is None:
2799 value = (None, None)
2800 self._bounds[0].render_input(varprefix + "_from", value[0])
2801 html.nbsp()
2802 html.write_text("-")
2803 html.nbsp()
2804 self._bounds[1].render_input(varprefix + "_until", value[1])
2806 def value_to_text(self, value):
2807 if value is None:
2808 return ""
2810 return self._bounds[0].value_to_text(value[0]) + "-" + \
2811 self._bounds[1].value_to_text(value[1])
2813 def from_html_vars(self, varprefix):
2814 from_value = self._bounds[0].from_html_vars(varprefix + "_from")
2815 until_value = self._bounds[1].from_html_vars(varprefix + "_until")
2816 if (from_value is None) != (until_value is None):
2817 raise MKUserError(
2818 varprefix + "_from",
2819 _("Please leave either both from and until empty or enter two times."))
2820 if from_value is None:
2821 return None
2822 return (from_value, until_value)
2824 def validate_datatype(self, value, varprefix):
2825 if self._allow_empty and value is None:
2826 return
2828 if not isinstance(value, tuple):
2829 raise MKUserError(varprefix,
2830 _("The datatype must be tuple, but ist %s") % _type_name(value))
2832 if len(value) != 2:
2833 raise MKUserError(
2834 varprefix,
2835 _("The tuple must contain two elements, but you have %d") % len(value))
2837 self._bounds[0].validate_datatype(value[0], varprefix + "_from")
2838 self._bounds[1].validate_datatype(value[1], varprefix + "_until")
2840 def validate_value(self, value, varprefix):
2841 if value is None:
2842 if self._allow_empty:
2843 return
2844 else:
2845 raise MKUserError(varprefix + "_from", _("Please enter a valid time of day range"))
2846 return
2848 self._bounds[0].validate_value(value[0], varprefix + "_from")
2849 self._bounds[1].validate_value(value[1], varprefix + "_until")
2850 if value[0] > value[1]:
2851 raise MKUserError(
2852 varprefix + "_until",
2853 _("The <i>from</i> time must not be later then the <i>until</i> time."))
2854 ValueSpec.custom_validate(self, value, varprefix)
2857 class TimeHelper(object):
2858 @staticmethod
2859 def round(timestamp, unit):
2860 time_s = list(time.localtime(timestamp))
2861 time_s[3] = time_s[4] = time_s[5] = 0
2862 time_s[8] = -1
2863 if unit == 'd':
2864 return time.mktime(time_s)
2865 elif unit == 'w':
2866 days = time_s[6] # 0 based
2867 elif unit == 'm':
2868 days = time_s[2] - 1 # 1 based
2869 elif unit == 'y':
2870 days = time_s[7] - 1 # 1 based
2871 else:
2872 raise MKGeneralException("invalid time unit %s" % unit)
2874 return TimeHelper.round(time.mktime(time_s) - days * 86400 + 3600, 'd')
2876 @staticmethod
2877 def add(timestamp, count, unit):
2878 if unit == 'h':
2879 return timestamp + 3600 * count
2880 elif unit == 'd':
2881 return timestamp + 86400 * count
2882 elif unit == 'w':
2883 return timestamp + (7 * 86400) * count
2884 elif unit == 'm':
2885 time_s = list(time.localtime(timestamp))
2886 years, months = divmod(abs(count), 12)
2888 if count < 0:
2889 years *= -1
2890 months *= -1
2892 time_s[0] += years
2893 time_s[1] += months
2894 if time_s[1] <= 0:
2895 time_s[0] -= 1
2896 time_s[1] = 12 - time_s[1]
2897 time_s[8] = -1
2898 return time.mktime(time_s)
2899 elif unit == 'y':
2900 time_s = list(time.localtime(timestamp))
2901 time_s[0] += count
2902 return time.mktime(time_s)
2903 else:
2904 MKGeneralException("invalid time unit %s" % unit)
2907 class Timerange(CascadingDropdown):
2908 def __init__(self, **kwargs):
2909 self._title = _('Time range')
2910 self._allow_empty = kwargs.get("allow_empty", False)
2911 self._include_time = kwargs.get("include_time", False)
2912 self._fixed_choices = kwargs.get("choices", [])
2913 kwargs['choices'] = self._prepare_choices
2914 CascadingDropdown.__init__(self, **kwargs)
2916 def _prepare_choices(self):
2917 choices = list(self._fixed_choices)
2919 if self._allow_empty:
2920 choices += [(None, '')]
2922 choices += self._get_graph_timeranges() + [
2923 ("d0", _("Today")),
2924 ("d1", _("Yesterday")),
2925 ("w0", _("This week")),
2926 ("w1", _("Last week")),
2927 ("m0", _("This month")),
2928 ("m1", _("Last month")),
2929 ("y0", _("This year")),
2930 ("y1", _("Last year")),
2931 ("age", _("The last..."), Age()),
2932 ("date", _("Date range"),
2933 Tuple(
2934 orientation="horizontal",
2935 title_br=False,
2936 elements=[
2937 AbsoluteDate(title=_("From:")),
2938 AbsoluteDate(title=_("To:")),
2939 ])),
2942 if self._include_time:
2943 choices += [("time", _("Date & time range"),
2944 Tuple(
2945 orientation="horizontal",
2946 title_br=False,
2947 elements=[
2948 AbsoluteDate(
2949 title=_("From:"),
2950 include_time=True,
2952 AbsoluteDate(
2953 title=_("To:"),
2954 include_time=True,
2956 ]))]
2957 return choices
2959 def _get_graph_timeranges(self):
2960 try:
2961 import cmk.gui.config as config # FIXME
2962 return [(('age', timerange_attrs["duration"]), timerange_attrs['title'])
2963 for timerange_attrs in config.graph_timeranges]
2965 except AttributeError: # only available in cee
2966 return [
2967 ("4h", _("The last 4 hours")),
2968 ("25h", _("The last 25 hours")),
2969 ("8d", _("The last 8 days")),
2970 ("35d", _("The last 35 days")),
2971 ("400d", _("The last 400 days")),
2974 def compute_range(self, rangespec):
2975 if rangespec is None:
2976 rangespec = "4h"
2978 # Compatibility with previous versions
2979 elif rangespec[0] == "pnp_view":
2980 rangespec = {
2981 1: "4h",
2982 2: "25h",
2983 3: "8d",
2984 4: "35d",
2985 5: "400d",
2986 }.get(rangespec[1], "4h")
2988 now = time.time()
2990 if rangespec[0] == 'age':
2991 from_time = now - rangespec[1]
2992 until_time = now
2993 title = _("The last ") + Age().value_to_text(rangespec[1])
2994 return (from_time, until_time), title
2996 elif rangespec[0] in ['date', 'time']:
2997 from_time, until_time = rangespec[1]
2998 if from_time > until_time:
2999 raise MKUserError("avo_rangespec_9_0_year",
3000 _("The end date must be after the start date"))
3001 if rangespec[0] == 'date':
3002 # add 25 hours, then round to 00:00 of that day. This accounts for
3003 # daylight-saving time
3004 until_time = TimeHelper.round(TimeHelper.add(until_time, 25, 'h'), 'd')
3005 title = AbsoluteDate().value_to_text(from_time) + " ... " + \
3006 AbsoluteDate().value_to_text(until_time)
3007 return (from_time, until_time), title
3009 else:
3010 until_time = now
3011 if rangespec[0].isdigit(): # 4h, 400d
3012 count = int(rangespec[:-1])
3013 from_time = TimeHelper.add(now, count * -1, rangespec[-1])
3014 unit_name = {'d': "days", 'h': "hours"}[rangespec[-1]]
3015 title = _("Last %d %s") % (count, unit_name)
3016 return (from_time, now), title
3018 year, month = time.localtime(now)[:2]
3019 # base time is current time rounded down to the nearest unit (day, week, ...)
3020 from_time = TimeHelper.round(now, rangespec[0])
3021 # derive titles from unit ()
3022 titles = {
3023 'd': (_("Today"), _("Yesterday")),
3024 'w': (_("This week"), _("Last week")),
3025 'y': (str(year), str(year - 1)),
3026 'm': ("%s %d" % (defines.month_name(month - 1), year),
3027 "%s %d" % (defines.month_name((month + 10) % 12), year - int(month == 1))),
3028 }[rangespec[0]]
3030 if rangespec[1] == '0':
3031 return (from_time, now), titles[0]
3033 # last (previous)
3034 prev_time = TimeHelper.add(from_time, -1, rangespec[0])
3035 # add one hour to the calculated time so that if dst started in that period,
3036 # we don't round down a whole day
3037 prev_time = TimeHelper.round(prev_time + 3600, 'd')
3038 return (prev_time, from_time), titles[1]
3041 # A selection of various date formats
3042 def DateFormat(**args):
3043 args.setdefault("title", _("Date format"))
3044 args.setdefault("default_value", "%Y-%m-%d")
3045 args["choices"] = [
3046 ("%Y-%m-%d", "1970-12-18"),
3047 ("%d.%m.%Y", "18.12.1970"),
3048 ("%m/%d/%Y", "12/18/1970"),
3049 ("%d.%m.", "18.12."),
3050 ("%m/%d", "12/18"),
3052 return DropdownChoice(**args)
3055 def TimeFormat(**args):
3056 args.setdefault("title", _("Time format"))
3057 args.setdefault("default_value", "%H:%M:%S")
3058 args["choices"] = [
3059 ("%H:%M:%S", "18:27:36"),
3060 ("%l:%M:%S %p", "12:27:36 PM"),
3061 ("%H:%M", "18:27"),
3062 ("%l:%M %p", "6:27 PM"),
3063 ("%H", "18"),
3064 ("%l %p", "6 PM"),
3066 return DropdownChoice(**args)
3069 # Make a configuration value optional, i.e. it may be None.
3070 # The user has a checkbox for activating the option. Example:
3071 # debug_log: it is either None or set to a filename.
3072 class Optional(ValueSpec):
3073 def __init__(self, valuespec, **kwargs):
3074 ValueSpec.__init__(self, **kwargs)
3075 self._valuespec = valuespec
3076 self._label = kwargs.get("label")
3077 self._negate = kwargs.get("negate", False)
3078 self._none_label = kwargs.get("none_label", _("(unset)"))
3079 self._none_value = kwargs.get("none_value", None)
3080 self._sameline = kwargs.get("sameline", False)
3081 self._indent = kwargs.get("indent", True)
3083 def canonical_value(self):
3084 return self._none_value
3086 def render_input(self, varprefix, value):
3087 self.classtype_info()
3088 div_id = "option_" + varprefix
3089 checked = html.get_checkbox(varprefix + "_use")
3090 if checked is None:
3091 if self._negate:
3092 checked = value == self._none_value
3093 else:
3094 checked = value != self._none_value
3096 html.open_span()
3098 if self._label is not None:
3099 label = self._label
3100 elif self.title():
3101 label = self.title()
3102 elif self._negate:
3103 label = _(" Ignore this option")
3104 else:
3105 label = _(" Activate this option")
3107 html.checkbox(
3108 "%s_use" % varprefix,
3109 checked,
3110 label=label,
3111 onclick="cmk.valuespecs.toggle_option(this, %r, %r)" % (div_id,
3112 1 if self._negate else 0))
3114 if self._sameline:
3115 html.nbsp()
3116 else:
3117 html.br()
3118 html.close_span()
3120 if self._indent:
3121 indent = 40
3122 else:
3123 indent = 0
3125 html.open_span(
3126 id_=div_id,
3127 style=[
3128 "margin-left: %dpx;" % indent, "display:none;" if checked == self._negate else None
3130 if value == self._none_value:
3131 value = self._valuespec.default_value()
3132 if self._valuespec.title():
3133 html.write(self._valuespec.title() + " ")
3134 self._valuespec.render_input(varprefix + "_value", value)
3135 html.close_span()
3137 def value_to_text(self, value):
3138 if value == self._none_value:
3139 return self._none_label
3140 return self._valuespec.value_to_text(value)
3142 def from_html_vars(self, varprefix):
3143 checkbox_checked = html.get_checkbox(varprefix + "_use") is True # not None or False
3144 if checkbox_checked != self._negate:
3145 return self._valuespec.from_html_vars(varprefix + "_value")
3146 return self._none_value
3148 def validate_datatype(self, value, varprefix):
3149 if value != self._none_value:
3150 self._valuespec.validate_datatype(value, varprefix + "_value")
3152 def validate_value(self, value, varprefix):
3153 if value != self._none_value:
3154 self._valuespec.validate_value(value, varprefix + "_value")
3155 ValueSpec.custom_validate(self, value, varprefix)
3158 # Makes a configuration value optional, while displaying the current
3159 # value as text with a checkbox in front of it. When the checkbox is being checked,
3160 # the text hides and the encapsulated valuespec is being shown.
3161 class OptionalEdit(Optional):
3162 def __init__(self, valuespec, **kwargs):
3163 Optional.__init__(self, valuespec, **kwargs)
3164 self._label = ''
3166 def render_input(self, varprefix, value):
3167 self.classtype_info()
3168 div_id = "option_" + varprefix
3169 checked = html.get_checkbox(varprefix + "_use")
3170 if checked is None:
3171 checked = self._negate
3173 html.open_span()
3175 if self._label is not None:
3176 label = self._label
3177 elif self.title():
3178 label = self.title()
3179 elif self._negate:
3180 label = _(" Ignore this option")
3181 else:
3182 label = _(" Activate this option")
3184 html.checkbox(
3185 "%s_use" % varprefix,
3186 checked,
3187 label=label,
3188 onclick=
3189 "cmk.valuespecs.toggle_option(this, %r, %r);cmk.valuespecs.toggle_option(this, %r, %r)"
3190 % (div_id + '_on', 1 if self._negate else 0, div_id + '_off', 0 if self._negate else 1))
3192 html.nbsp()
3193 html.close_span()
3195 if value is None:
3196 value = self._valuespec.default_value()
3198 html.open_span(
3199 id_="%s_off" % div_id, style="display:none;" if checked != self._negate else None)
3200 html.write(value)
3201 html.close_span()
3203 html.open_span(
3204 id_="%s_on" % div_id, style="display:none;" if checked == self._negate else None)
3205 if self._valuespec.title():
3206 html.write(self._valuespec.title() + " ")
3207 self._valuespec.render_input(varprefix + "_value", value)
3208 html.close_span()
3210 def from_html_vars(self, varprefix):
3211 return self._valuespec.from_html_vars(varprefix + "_value")
3214 # Handle case when there are several possible allowed formats
3215 # for the value (e.g. strings, 4-tuple or 6-tuple like in SNMP-Communities)
3216 # The different alternatives must have different data types that can
3217 # be distinguished with validate_datatype.
3218 class Alternative(ValueSpec):
3219 def __init__(self, **kwargs):
3220 super(Alternative, self).__init__(**kwargs)
3221 self._elements = kwargs["elements"]
3222 self._match = kwargs.get("match") # custom match function, returns index in elements
3223 self._style = kwargs.get("style", "radio") # alternative: "dropdown"
3224 self._show_alternative_title = kwargs.get("show_alternative_title")
3225 self._on_change = kwargs.get("on_change") # currently only working for style="dropdown"
3226 self._orientation = kwargs.get("orientation",
3227 "vertical") # or horizontal: for style="dropdown"
3229 # Return the alternative (i.e. valuespec)
3230 # that matches the datatype of a given value. We assume
3231 # that always one matches. No error handling here.
3232 # This may also tranform the input value in case it gets
3233 # "decorated" in the from_html_vars function
3234 def matching_alternative(self, value):
3235 if self._match:
3236 return self._elements[self._match(value)], value
3238 for vs in self._elements:
3239 try:
3240 vs.validate_datatype(value, "")
3241 return vs, value
3242 except:
3243 pass
3245 return None, value
3247 def render_input(self, varprefix, value):
3248 self.classtype_info()
3249 if self._style == "radio":
3250 self.render_input_radio(varprefix, value)
3251 else:
3252 self.render_input_dropdown(varprefix, value)
3254 def render_input_dropdown(self, varprefix, value):
3255 mvs, value = self.matching_alternative(value)
3256 options = []
3257 sel_option = html.request.var(varprefix + "_use")
3258 for nr, vs in enumerate(self._elements):
3259 if not sel_option and vs == mvs:
3260 sel_option = str(nr)
3261 options.append((str(nr), vs.title()))
3262 onchange = "cmk.valuespecs.cascading_change(this, '%s', %d);" % (varprefix, len(options))
3263 if self._on_change:
3264 onchange += self._on_change
3265 if self._orientation == "horizontal":
3266 html.open_table()
3267 html.open_tr()
3268 html.open_td()
3269 html.dropdown(varprefix + "_use", options, deflt=sel_option, onchange=onchange)
3270 if self._orientation == "vertical":
3271 html.br()
3272 html.br()
3274 for nr, vs in enumerate(self._elements):
3275 if str(nr) == sel_option:
3276 disp = ""
3277 cur_val = value
3278 else:
3279 disp = "none"
3280 cur_val = vs.default_value()
3282 if self._orientation == "horizontal":
3283 html.close_td()
3284 html.open_td()
3285 html.open_span(id_="%s_%s_sub" % (varprefix, nr), style="display:%s" % disp)
3286 html.help(vs.help())
3287 vs.render_input(varprefix + "_%d" % nr, cur_val)
3288 html.close_span()
3290 if self._orientation == "horizontal":
3291 html.close_td()
3292 html.close_tr()
3293 html.close_table()
3295 def render_input_radio(self, varprefix, value):
3296 mvs, value = self.matching_alternative(value)
3297 for nr, vs in enumerate(self._elements):
3298 if html.request.has_var(varprefix + "_use"):
3299 checked = html.request.var(varprefix + "_use") == str(nr)
3300 else:
3301 checked = vs == mvs
3303 html.help(vs.help())
3304 title = vs.title()
3305 if not title and nr:
3306 html.nbsp()
3307 html.nbsp()
3309 html.radiobutton(varprefix + "_use", str(nr), checked, title)
3310 if title:
3311 html.open_ul()
3312 if vs == mvs:
3313 val = value
3314 else:
3315 val = vs.default_value()
3316 vs.render_input(varprefix + "_%d" % nr, val)
3317 if title:
3318 html.close_ul()
3320 def set_focus(self, varprefix):
3321 # TODO: Set focus to currently active option
3322 pass
3324 def canonical_value(self):
3325 return self._elements[0].canonical_value()
3327 def default_value(self):
3328 try:
3329 if isinstance(self._default_value, type(lambda: True)):
3330 return self._default_value()
3331 return self._default_value
3332 except:
3333 return self._elements[0].default_value()
3335 def value_to_text(self, value):
3336 vs, value = self.matching_alternative(value)
3337 if vs:
3338 output = ""
3339 if self._show_alternative_title and vs.title():
3340 output = "%s<br>" % vs.title()
3341 return output + vs.value_to_text(value)
3342 else:
3343 return _("invalid:") + " " + html.attrencode(str(value))
3345 def from_html_vars(self, varprefix):
3346 nr = int(html.request.var(varprefix + "_use"))
3347 vs = self._elements[nr]
3348 return vs.from_html_vars(varprefix + "_%d" % nr)
3350 def validate_datatype(self, value, varprefix):
3351 for vs in self._elements:
3352 try:
3353 vs.validate_datatype(value, "")
3354 return
3355 except:
3356 pass
3357 raise MKUserError(
3358 varprefix,
3359 _("The data type of the value does not match any of the "
3360 "allowed alternatives."))
3362 def validate_value(self, value, varprefix):
3363 vs, value = self.matching_alternative(value)
3364 for nr, v in enumerate(self._elements):
3365 if vs == v:
3366 vs.validate_value(value, varprefix + "_%d" % nr)
3367 ValueSpec.custom_validate(self, value, varprefix)
3370 # Edit a n-tuple (with fixed size) of values
3371 class Tuple(ValueSpec):
3372 def __init__(self, **kwargs):
3373 ValueSpec.__init__(self, **kwargs)
3374 self._elements = kwargs["elements"]
3375 self._show_titles = kwargs.get("show_titles", True)
3376 self._orientation = kwargs.get("orientation", "vertical") # also: horizontal, float
3377 self._separator = kwargs.get("separator", " ") # in case of float
3378 self._title_br = kwargs.get("title_br", True)
3380 def canonical_value(self):
3381 return tuple([x.canonical_value() for x in self._elements])
3383 def default_value(self):
3384 return tuple([x.default_value() for x in self._elements])
3386 def render_input(self, varprefix, value):
3387 self.classtype_info()
3388 if self._orientation != "float":
3389 html.open_table(class_=["valuespec_tuple", self._orientation])
3390 if self._orientation == "horizontal":
3391 html.open_tr()
3393 for no, element in enumerate(self._elements):
3394 try:
3395 val = value[no]
3396 except:
3397 val = element.default_value()
3398 vp = varprefix + "_" + str(no)
3399 if self._orientation == "vertical":
3400 html.open_tr()
3401 elif self._orientation == "float":
3402 html.write(self._separator)
3404 if self._show_titles:
3405 elem_title = element.title()
3406 if elem_title:
3407 title = element.title()[0].upper() + element.title()[1:]
3408 else:
3409 title = ""
3410 if self._orientation == "vertical":
3411 html.open_td(class_="tuple_left")
3412 html.write(title)
3414 html.help(element.help())
3415 html.close_td()
3416 elif self._orientation == "horizontal":
3417 html.open_td(class_="tuple_td")
3418 html.open_span(class_=["title"])
3419 html.write(title)
3421 html.help(element.help())
3422 html.close_span()
3423 if self._title_br:
3424 html.br()
3425 else:
3426 html.write_text(" ")
3427 else:
3428 html.write_text(" ")
3429 html.help(element.help())
3431 else:
3432 if self._orientation == "horizontal":
3433 html.open_td(class_="tuple_td")
3435 if self._orientation == "vertical":
3436 html.open_td(class_="tuple_right")
3438 element.render_input(vp, val)
3439 if self._orientation != "float":
3440 html.close_td()
3441 if self._orientation == "vertical":
3442 html.close_tr()
3443 if self._orientation == "horizontal":
3444 html.close_tr()
3445 if self._orientation != "float":
3446 html.close_table()
3448 def set_focus(self, varprefix):
3449 self._elements[0].set_focus(varprefix + "_0")
3451 def value_to_text(self, value):
3452 return "" + ", ".join(
3453 [element.value_to_text(val) for (element, val) in zip(self._elements, value)]) + ""
3455 def from_html_vars(self, varprefix):
3456 value = []
3457 for no, element in enumerate(self._elements):
3458 vp = varprefix + "_" + str(no)
3459 value.append(element.from_html_vars(vp))
3460 return tuple(value)
3462 def validate_value(self, value, varprefix):
3463 for no, (element, val) in enumerate(zip(self._elements, value)):
3464 vp = varprefix + "_" + str(no)
3465 element.validate_value(val, vp)
3466 ValueSpec.custom_validate(self, value, varprefix)
3468 def validate_datatype(self, value, varprefix):
3469 if not isinstance(value, tuple):
3470 raise MKUserError(varprefix,
3471 _("The datatype must be a tuple, but is %s") % _type_name(value))
3472 if len(value) != len(self._elements):
3473 raise MKUserError(
3474 varprefix,
3475 _("The number of elements in the tuple must be exactly %d.") % len(self._elements))
3477 for no, (element, val) in enumerate(zip(self._elements, value)):
3478 vp = varprefix + "_" + str(no)
3479 element.validate_datatype(val, vp)
3482 class Dictionary(ValueSpec):
3483 def __init__(self, **kwargs):
3484 super(Dictionary, self).__init__(**kwargs)
3485 self._elements = kwargs["elements"]
3486 self._empty_text = kwargs.get("empty_text", _("(no parameters)"))
3487 # Optionally a text can be specified to be shown by value_to_text()
3488 # when the value equal the default value of the value spec. Normally
3489 # the default values are shown.
3490 self._default_text = kwargs.get("default_text", None)
3491 self._required_keys = kwargs.get("required_keys", [])
3492 self._ignored_keys = kwargs.get("ignored_keys", [])
3493 self._default_keys = kwargs.get("default_keys", []) # keys present in default value
3494 if "optional_keys" in kwargs:
3495 ok = kwargs["optional_keys"]
3496 if isinstance(ok, list) and ok:
3497 self._required_keys = \
3498 [ e[0] for e in self._get_elements() if e[0] not in ok ]
3499 self._optional_keys = True
3500 elif ok:
3501 self._optional_keys = True
3502 else:
3503 self._optional_keys = False
3504 else:
3505 self._optional_keys = True
3506 if "hidden_keys" in kwargs:
3507 self._hidden_keys = kwargs["hidden_keys"]
3508 else:
3509 self._hidden_keys = []
3511 self._columns = kwargs.get("columns", 1) # possible: 1 or 2
3512 self._render = kwargs.get("render", "normal") # also: "form" -> use forms.section()
3513 self._form_narrow = kwargs.get("form_narrow", False) # used if render == "form"
3514 self._form_isopen = kwargs.get("form_isopen", True) # used if render == "form"
3515 self._headers = kwargs.get("headers") # "sup" -> small headers in oneline mode
3516 self._migrate = kwargs.get("migrate") # value migration from old tuple version
3517 self._indent = kwargs.get("indent", True)
3519 def migrate(self, value):
3520 if self._migrate:
3521 return self._migrate(value)
3522 return value
3524 def _get_elements(self):
3525 if callable(self._elements) or isinstance(self._elements, types.MethodType):
3526 return self._elements()
3527 elif isinstance(self._elements, list):
3528 return self._elements
3529 return []
3531 def render_input_as_form(self, varprefix, value):
3532 self.classtype_info()
3533 value = self.migrate(value)
3534 if not isinstance(value, (dict, DictMixin)):
3535 value = {} # makes code simpler in complain phase
3537 self._render_input_form(varprefix, value)
3539 def render_input(self, varprefix, value):
3540 self.classtype_info()
3541 value = self.migrate(value)
3542 if not isinstance(value, (dict, DictMixin)):
3543 value = {} # makes code simpler in complain phase
3545 if self._render == "form":
3546 self._render_input_form(varprefix, value)
3547 elif self._render == "form_part":
3548 self._render_input_form(varprefix, value, as_part=True)
3549 else:
3550 self._render_input_normal(varprefix, value, self._render == "oneline")
3552 def _render_input_normal(self, varprefix, value, oneline=False):
3553 headers_sup = oneline and self._headers == "sup"
3554 if headers_sup or not oneline:
3555 html.open_table(class_=["dictionary"])
3556 if headers_sup:
3557 html.open_tr()
3558 for param, vs in self._get_elements():
3559 if param in self._hidden_keys:
3560 continue
3561 if not oneline:
3562 html.open_tr()
3563 html.open_td(class_="dictleft")
3565 div_id = varprefix + "_d_" + param
3566 vp = varprefix + "_p_" + param
3567 colon_printed = False
3568 if self._optional_keys and param not in self._required_keys:
3569 visible = html.get_checkbox(vp + "_USE")
3570 if visible is None:
3571 visible = param in value
3572 label = vs.title()
3573 if self._columns == 2:
3574 label += ":"
3575 colon_printed = True
3576 html.checkbox(
3577 "%s_USE" % vp,
3578 visible,
3579 label=label,
3580 onclick="cmk.valuespecs.toggle_option(this, %r)" % div_id)
3581 else:
3582 visible = True
3583 if vs.title():
3584 if headers_sup:
3585 html.open_td()
3586 html.open_b(class_=["header"])
3587 html.write(" %s" % vs.title())
3588 if oneline:
3589 if self._headers == "sup":
3590 html.close_b()
3591 html.br()
3592 else:
3593 html.write_text(": ")
3595 if self._columns == 2:
3596 if vs.title() and not colon_printed:
3597 html.write_text(':')
3598 html.help(vs.help())
3599 if not oneline:
3600 html.close_td()
3601 html.open_td(class_="dictright")
3603 else:
3604 if not oneline:
3605 html.br()
3607 html.open_div(
3608 id_=div_id,
3609 class_=["dictelement", "indent" if (self._indent and self._columns == 1) else None],
3610 style="display:none;" if not visible else
3611 ("display:inline-block;" if oneline else None))
3613 if self._columns == 1:
3614 html.help(vs.help())
3615 # Remember: in complain mode we do not render 'value' (the default value),
3616 # but re-display the values from the HTML variables. We must not use 'value'
3617 # in that case.
3618 if isinstance(value, dict):
3619 vs.render_input(vp, value.get(param, vs.default_value()))
3620 else:
3621 vs.render_input(vp, None)
3622 html.close_div()
3623 if not oneline:
3624 html.close_td()
3625 html.close_tr()
3626 elif headers_sup:
3627 html.close_td()
3629 if not oneline:
3630 html.close_table()
3631 elif oneline and self._headers == "sup":
3632 html.close_tr()
3633 html.close_table()
3635 def _render_input_form(self, varprefix, value, as_part=False):
3636 if self._headers:
3637 for entry in self._headers:
3638 if len(entry) == 2:
3639 header, section_elements = entry
3640 css = None
3641 else:
3642 header, css, section_elements = entry
3643 self.render_input_form_header(
3644 varprefix, value, header, section_elements, as_part, css=css)
3645 else:
3646 self.render_input_form_header(
3647 varprefix, value, self.title() or _("Properties"), None, as_part, css=None)
3649 if not as_part:
3650 forms.end()
3652 def render_input_form_header(self, varprefix, value, title, section_elements, as_part, css):
3653 if not as_part:
3654 forms.header(title, isopen=self._form_isopen, narrow=self._form_narrow)
3656 for param, vs in self._get_elements():
3657 if param in self._hidden_keys:
3658 continue
3660 if section_elements and param not in section_elements:
3661 continue
3663 div_id = varprefix + "_d_" + param
3664 vp = varprefix + "_p_" + param
3665 if self._optional_keys and param not in self._required_keys:
3666 visible = html.get_checkbox(vp + "_USE")
3667 if visible is None:
3668 visible = param in value
3669 checkbox_code = html.render_checkbox(
3670 vp + "_USE",
3671 deflt=visible,
3672 onclick="cmk.valuespecs.toggle_option(this, %r)" % div_id)
3673 forms.section(vs.title(), checkbox=checkbox_code, css=css)
3674 else:
3675 visible = True
3676 forms.section(vs.title(), css=css)
3678 html.open_div(id_=div_id, style="display:none;" if not visible else None)
3679 html.help(vs.help())
3680 vs.render_input(vp, value.get(param, vs.default_value()))
3681 html.close_div()
3683 def set_focus(self, varprefix):
3684 elements = self._get_elements()
3685 if elements:
3686 elements[0][1].set_focus(varprefix + "_p_" + elements[0][0])
3688 def canonical_value(self):
3689 return dict([(name, vs.canonical_value())
3690 for (name, vs) in self._get_elements()
3691 if name in self._required_keys or not self._optional_keys])
3693 def default_value(self):
3694 def_val = {}
3695 for name, vs in self._get_elements():
3696 if name in self._required_keys or not self._optional_keys or name in self._default_keys:
3697 def_val[name] = vs.default_value()
3699 return def_val
3701 def value_to_text(self, value):
3702 value = self.migrate(value)
3703 oneline = self._render == "oneline"
3704 if not value:
3705 return self._empty_text
3707 if self._default_text and value == self.default_value():
3708 return self._default_text
3710 elem = self._get_elements()
3711 s = '' if oneline else HTML()
3712 for param, vs in elem:
3713 if param in value:
3714 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
3715 text = HTML(vs.value_to_text(value[param]))
3716 if oneline:
3717 if param != elem[0][0]:
3718 s += ", "
3719 s += "%s: %s" % (vs.title(), text)
3720 else:
3721 s += html.render_tr(
3722 html.render_td("%s:&nbsp;" % vs.title(), class_="title") +
3723 html.render_td(text))
3724 if not oneline:
3725 s = html.render_table(s)
3726 return "%s" % s
3728 def from_html_vars(self, varprefix):
3729 value = {}
3730 for param, vs in self._get_elements():
3731 vp = varprefix + "_p_" + param
3732 if not self._optional_keys \
3733 or param in self._required_keys \
3734 or html.get_checkbox(vp + "_USE"):
3735 value[param] = vs.from_html_vars(vp)
3736 return value
3738 def validate_datatype(self, value, varprefix):
3739 value = self.migrate(value)
3741 if not isinstance(value, dict):
3742 raise MKUserError(
3743 varprefix,
3744 _("The type must be a dictionary, but it is a %s") % _type_name(value))
3746 for param, vs in self._get_elements():
3747 if param in value:
3748 vp = varprefix + "_p_" + param
3749 try:
3750 vs.validate_datatype(value[param], vp)
3751 except MKUserError as e:
3752 raise MKUserError(e.varname, _("%s: %s") % (vs.title(), e))
3753 elif not self._optional_keys or param in self._required_keys:
3754 raise MKUserError(varprefix, _("The entry %s is missing") % vs.title())
3756 # Check for exceeding keys
3757 allowed_keys = [p for p, _v in self._get_elements()]
3758 if self._ignored_keys:
3759 allowed_keys += self._ignored_keys
3760 for param in value.keys():
3761 if param not in allowed_keys:
3762 raise MKUserError(
3763 varprefix,
3764 _("Undefined key '%s' in the dictionary. Allowed are %s.") %
3765 (param, ", ".join(allowed_keys)))
3767 def validate_value(self, value, varprefix):
3768 value = self.migrate(value)
3770 for param, vs in self._get_elements():
3771 if param in value:
3772 vp = varprefix + "_p_" + param
3773 vs.validate_value(value[param], vp)
3774 elif not self._optional_keys or param in self._required_keys:
3775 raise MKUserError(varprefix, _("The entry %s is missing") % vs.title())
3776 ValueSpec.custom_validate(self, value, varprefix)
3779 # Base class for selection of a Nagios element out
3780 # of a given list that must be loaded from a file.
3781 # Example: GroupSelection. Child class must define
3782 # a function get_elements() that returns a dictionary
3783 # from element keys to element titles.
3784 class ElementSelection(ValueSpec):
3785 def __init__(self, **kwargs):
3786 ValueSpec.__init__(self, **kwargs)
3787 self._loaded_at = None
3788 self._label = kwargs.get("label")
3789 self._empty_text = kwargs.get("empty_text",
3790 _("There are no elements defined for this selection yet."))
3792 def load_elements(self):
3793 if self._loaded_at != id(html):
3794 self._elements = self.get_elements()
3795 self._loaded_at = id(html) # unique for each query!
3797 def get_elements(self):
3798 raise NotImplementedError()
3800 def canonical_value(self):
3801 self.load_elements()
3802 if len(self._elements) > 0:
3803 return self._elements.keys()[0]
3805 def render_input(self, varprefix, value):
3806 self.classtype_info()
3807 self.load_elements()
3808 if len(self._elements) == 0:
3809 html.write(self._empty_text)
3810 else:
3811 if self._label:
3812 html.write("%s" % self._label)
3813 html.nbsp()
3814 html.dropdown(varprefix, self._elements.items(), deflt=value, ordered=True)
3816 def value_to_text(self, value):
3817 self.load_elements()
3818 return html.attrencode(self._elements.get(value, value))
3820 def from_html_vars(self, varprefix):
3821 return html.request.var(varprefix)
3823 def validate_value(self, value, varprefix):
3824 self.load_elements()
3825 if len(self._elements) == 0:
3826 raise MKUserError(varprefix, _("You cannot save this rule.") + ' ' + self._empty_text)
3827 if value not in self._elements:
3828 raise MKUserError(varprefix,
3829 _("%s is not an existing element in this selection.") % (value,))
3830 ValueSpec.custom_validate(self, value, varprefix)
3832 def validate_datatype(self, value, varprefix):
3833 self.load_elements()
3834 # When no elements exists the default value is None and e.g. in wato.mode_edit_rule()
3835 # handed over to validate_datatype() before rendering the input form. Disable the
3836 # validation in this case to prevent validation errors. A helpful message is shown
3837 # during render_input()
3838 if len(self._elements) == 0 and value is None:
3839 return
3841 if not isinstance(value, str):
3842 raise MKUserError(varprefix,
3843 _("The datatype must be str (string), but is %s") % _type_name(value))
3846 class AutoTimestamp(FixedValue):
3847 def __init__(self, **kwargs):
3848 FixedValue.__init__(self, **kwargs)
3850 def canonical_value(self):
3851 return time.time()
3853 def from_html_vars(self, varprefix):
3854 return time.time()
3856 def value_to_text(self, value):
3857 return time.strftime("%F %T", time.localtime(value))
3859 def validate_datatype(self, value, varprefix):
3860 if not isinstance(value, (int, float)):
3861 return MKUserError(varprefix, _("Invalid datatype of timestamp: must be int or float."))
3864 # Fully transparant VS encapsulating a vs in a foldable
3865 # container.
3866 class Foldable(ValueSpec):
3867 def __init__(self, valuespec, **kwargs):
3868 ValueSpec.__init__(self, **kwargs)
3869 self._valuespec = valuespec
3870 self._open = kwargs.get("open", False)
3871 self._title_function = kwargs.get("title_function", None)
3873 def render_input(self, varprefix, value):
3874 self.classtype_info()
3875 try:
3876 title_value = value
3877 if html.form_submitted():
3878 try:
3879 title_value = self._valuespec.from_html_vars(varprefix)
3880 except:
3881 pass
3882 title = self._title_function(title_value)
3883 except:
3884 title = self._valuespec.title()
3885 if not title:
3886 title = _("(no title)")
3887 html.begin_foldable_container("valuespec_foldable", varprefix, self._open, title, False)
3888 html.help(self._valuespec.help())
3889 self._valuespec.render_input(varprefix, value)
3890 html.end_foldable_container()
3892 def set_focus(self, varprefix):
3893 self._valuespec.set_focus(varprefix)
3895 def canonical_value(self):
3896 return self._valuespec.canonical_value()
3898 def default_value(self):
3899 return self._valuespec.default_value()
3901 def value_to_text(self, value):
3902 return self._valuespec.value_to_text(value)
3904 def from_html_vars(self, varprefix):
3905 return self._valuespec.from_html_vars(varprefix)
3907 def validate_datatype(self, value, varprefix):
3908 self._valuespec.validate_datatype(value, varprefix)
3910 def validate_value(self, value, varprefix):
3911 self._valuespec.validate_value(value, varprefix)
3912 ValueSpec.custom_validate(self, value, varprefix)
3915 # Transforms the value from one representation to
3916 # another while being completely transparent to the user.
3917 # forth: function that converts a value into the representation
3918 # needed by the encapsulated vs
3919 # back: function that converts a value created by the encapsulated
3920 # vs back to the outer representation
3923 class Transform(ValueSpec):
3924 def __init__(self, valuespec, **kwargs):
3925 ValueSpec.__init__(self, **kwargs)
3926 self._valuespec = valuespec
3927 self._back = kwargs.get("back")
3928 self._forth = kwargs.get("forth")
3930 def forth(self, value):
3931 if self._forth:
3932 return self._forth(value)
3933 return value
3935 def back(self, value):
3936 if self._back:
3937 return self._back(value)
3938 return value
3940 def title(self):
3941 if self._title:
3942 return self._title
3943 return self._valuespec.title()
3945 def help(self):
3946 if self._help:
3947 return self._help
3948 return self._valuespec.help()
3950 def render_input(self, varprefix, value):
3951 self.classtype_info()
3952 self._valuespec.render_input(varprefix, self.forth(value))
3954 def render_input_as_form(self, varprefix, value):
3955 if not isinstance(self._valuespec, Dictionary):
3956 raise NotImplementedError()
3957 self.classtype_info()
3958 self._valuespec.render_input_as_form(varprefix, self.forth(value))
3960 def set_focus(self, varprefix):
3961 self._valuespec.set_focus(varprefix)
3963 def canonical_value(self):
3964 return self.back(self._valuespec.canonical_value())
3966 def default_value(self):
3967 return self.back(self._valuespec.default_value())
3969 def value_to_text(self, value):
3970 return self._valuespec.value_to_text(self.forth(value))
3972 def from_html_vars(self, varprefix):
3973 return self.back(self._valuespec.from_html_vars(varprefix))
3975 def validate_datatype(self, value, varprefix):
3976 self._valuespec.validate_datatype(self.forth(value), varprefix)
3978 def validate_value(self, value, varprefix):
3979 self._valuespec.validate_value(self.forth(value), varprefix)
3980 ValueSpec.custom_validate(self, value, varprefix)
3983 class LDAPDistinguishedName(TextUnicode):
3984 def __init__(self, **kwargs):
3985 TextUnicode.__init__(self, **kwargs)
3986 self.enforce_suffix = kwargs.get('enforce_suffix')
3988 def validate_value(self, value, varprefix):
3989 TextAscii.validate_value(self, value, varprefix)
3991 # Check whether or not the given DN is below a base DN
3992 if self.enforce_suffix and value and not value.lower().endswith(
3993 self.enforce_suffix.lower()):
3994 raise MKUserError(varprefix, _('Does not ends with "%s".') % self.enforce_suffix)
3995 ValueSpec.custom_validate(self, value, varprefix)
3998 class Password(TextAscii):
3999 def __init__(self, **kwargs):
4000 self._is_stored_plain = kwargs.get("is_stored_plain", True)
4001 kwargs.setdefault("autocomplete", False)
4003 if self._is_stored_plain:
4004 plain_help = _("The password entered here is stored in plain text within the "
4005 "monitoring site. This usually needed because the monitoring "
4006 "process needs to have access to the unencrypted password "
4007 "because it needs to submit it to authenticate with remote systems. ")
4009 if "help" in kwargs:
4010 kwargs["help"] += "<br><br>" + plain_help
4011 else:
4012 kwargs["help"] = plain_help
4014 TextAscii.__init__(self, attrencode=True, **kwargs)
4016 def render_input(self, varprefix, value):
4017 self.classtype_info()
4018 if value is None:
4019 value = ""
4021 if self._label:
4022 html.write(self._label)
4023 html.nbsp()
4025 kwargs = {
4026 "size": self._size,
4029 if self._autocomplete is False:
4030 kwargs["autocomplete"] = "new-password"
4032 html.password_input(varprefix, str(value), **kwargs)
4034 def password_plaintext_warning(self):
4035 if self._is_stored_plain:
4036 html.span(
4037 _("<br>Please note that Check_MK needs this password in clear"
4038 "<br>text during normal operation and thus stores it unencrypted"
4039 "<br>on the Check_MK server."))
4041 def value_to_text(self, value):
4042 if value is None:
4043 return _("none")
4044 return '******'
4047 class PasswordSpec(Password):
4048 def __init__(self, hidden=True, **kwargs):
4049 super(PasswordSpec, self).__init__(hidden=hidden, **kwargs)
4051 def render_input(self, varprefix, value):
4052 self.classtype_info()
4053 TextAscii.render_input(self, varprefix, value)
4054 if not value:
4055 html.icon_button(
4056 "#",
4057 _(u"Randomize password"),
4058 "random",
4059 onclick="cmk.valuespecs.passwordspec_randomize(this);")
4060 if self._hidden:
4061 html.icon_button(
4062 "#",
4063 _(u"Show/Hide password"),
4064 "showhide",
4065 onclick="cmk.valuespecs.toggle_hidden(this);")
4067 self.password_plaintext_warning()
4070 class FileUpload(ValueSpec):
4071 def __init__(self, **kwargs):
4072 ValueSpec.__init__(self, **kwargs)
4073 self._allow_empty = kwargs.get('allow_empty', True)
4074 self._allowed_extensions = kwargs.get('allowed_extensions')
4075 self._allow_empty_content = kwargs.get('allow_empty_content', True)
4077 def canonical_value(self):
4078 if self._allow_empty:
4079 return None
4080 return ''
4082 def validate_value(self, value, varprefix):
4083 file_name, _mime_type, content = value
4085 if not self._allow_empty and (content == '' or file_name == ''):
4086 raise MKUserError(varprefix, _('Please select a file.'))
4088 if not self._allow_empty_content and len(content) == 0:
4089 raise MKUserError(varprefix,
4090 _('The selected file is empty. Please select a non-empty file.'))
4091 if self._allowed_extensions is not None:
4092 matched = False
4093 for extension in self._allowed_extensions:
4094 if file_name.endswith(extension):
4095 matched = True
4096 break
4097 if not matched:
4098 raise MKUserError(
4099 varprefix,
4100 _("Invalid file name extension. Allowed are: %s") % ", ".join(
4101 self._allowed_extensions))
4103 self.custom_validate(value, varprefix)
4105 def render_input(self, varprefix, value):
4106 self.classtype_info()
4107 html.upload_file(varprefix)
4109 def from_html_vars(self, varprefix):
4110 # returns a triple of (filename, mime-type, content)
4111 return html.request.uploaded_file(varprefix)
4114 class ImageUpload(FileUpload):
4115 def __init__(self, max_size=None, show_current_image=False, **kwargs):
4116 self._max_size = max_size
4117 self._show_current_image = show_current_image
4118 FileUpload.__init__(self, **kwargs)
4120 def render_input(self, varprefix, value):
4121 self.classtype_info()
4123 if self._show_current_image and value:
4124 html.open_table()
4125 html.open_tr()
4126 html.td(_("Current image:"))
4127 html.td(html.render_img("data:image/png;base64,%s" % base64.b64encode(value)))
4128 html.close_tr()
4129 html.open_tr()
4130 html.td(_("Upload new:"))
4131 html.open_td()
4132 super(ImageUpload, self).render_input(varprefix, value)
4133 html.close_td()
4134 html.close_tr()
4135 html.close_table()
4136 else:
4137 super(ImageUpload, self).render_input(varprefix, value)
4139 def validate_value(self, value, varprefix):
4140 file_name, mime_type, content = value
4142 if file_name[-4:] != '.png' \
4143 or mime_type != 'image/png' \
4144 or not content.startswith('\x89PNG'):
4145 raise MKUserError(varprefix, _('Please choose a PNG image.'))
4147 try:
4148 im = Image.open(StringIO(content))
4149 except IOError:
4150 raise MKUserError(varprefix, _('Please choose a valid PNG image.'))
4152 if self._max_size:
4153 w, h = im.size
4154 max_w, max_h = self._max_size
4155 if w > max_w or h > max_h:
4156 raise MKUserError(varprefix, _('Maximum image size: %dx%dpx') % (max_w, max_h))
4158 ValueSpec.custom_validate(self, value, varprefix)
4161 class UploadOrPasteTextFile(Alternative):
4162 def __init__(self, **kwargs):
4163 file_title = kwargs.get("file_title", _("File"))
4164 allow_empty = kwargs.get("allow_empty", True)
4165 kwargs["elements"] = [
4166 FileUpload(title=_("Upload %s") % file_title, allow_empty=allow_empty),
4167 TextAreaUnicode(
4168 title=_("Content of %s") % file_title,
4169 allow_empty=allow_empty,
4170 cols=80,
4171 rows="auto"),
4174 if kwargs.get("default_mode", "text") == "upload":
4175 kwargs["match"] = lambda *args: 0
4176 else:
4177 kwargs["match"] = lambda *args: 1
4179 kwargs.setdefault("style", "dropdown")
4180 Alternative.__init__(self, **kwargs)
4182 def from_html_vars(self, varprefix):
4183 value = Alternative.from_html_vars(self, varprefix)
4184 # Convert textarea value to format of upload field
4185 if not isinstance(value, tuple):
4186 value = (None, None, value)
4187 return value
4190 class TextOrRegExp(Alternative):
4191 _text_valuespec_class = TextAscii
4192 _regex_valuespec_class = RegExp
4194 def __init__(self, **kwargs):
4195 allow_empty = kwargs.pop("allow_empty", True)
4197 if "text_valuespec" in kwargs:
4198 vs_text = kwargs.pop("text_valuespec")
4199 else:
4200 vs_text = self._text_valuespec_class(
4201 title=_("Explicit match"),
4202 allow_empty=allow_empty,
4205 vs_regex = self._regex_valuespec_class(
4206 mode=RegExp.prefix,
4207 title=_("Regular expression match"),
4208 allow_empty=allow_empty,
4211 kwargs.update({
4212 "elements": [
4213 vs_text,
4214 Transform(
4215 vs_regex,
4216 forth=lambda v: v[1:], # strip of "~"
4217 back=lambda v: "~" + v, # add "~"
4220 # Use RegExp field when value is prefixed with "~"
4221 "match": lambda v: 1 if v and v[0] == "~" else 0,
4222 "style": "dropdown",
4223 "orientation": "horizontal",
4226 super(TextOrRegExp, self).__init__(**kwargs)
4229 class TextOrRegExpUnicode(TextOrRegExp):
4230 _default_valuespec_class = TextUnicode
4231 _regex_valuespec_class = RegExpUnicode
4234 class Labels(ValueSpec):
4235 """Valuespec to render and input a collection of object labels"""
4237 class World(Enum):
4238 CONFIG = "config"
4239 CORE = "core"
4241 class Source(Enum):
4242 EXPLICIT = "explicit"
4243 RULESET = "ruleset"
4244 DISCOVERED = "discovered"
4246 def __init__(self, world, label_source=None, **kwargs):
4247 self._world = world
4248 # Set this source to mark the labels that have no explicit label source set
4249 self._label_source = label_source
4250 kwargs.setdefault("help", "")
4251 kwargs["help"] += _("Labels need to be in the format <tt>[KEY]:[VALUE]</tt>. "
4252 "For example <tt>os:windows</tt>.")
4253 super(Labels, self).__init__(**kwargs)
4255 def canonical_value(self):
4256 return {}
4258 def from_html_vars(self, varprefix):
4259 return dict(
4260 e["value"].split(":", 1) for e in json.loads(html.get_unicode_input(varprefix) or "[]"))
4262 def value_to_text(self, value):
4263 from cmk.gui.view_utils import render_labels
4264 label_sources = {k: self._label_source.value for k in value.keys()
4265 } if self._label_source else {}
4266 return render_labels(value, "host", with_links=False, label_sources=label_sources)
4268 def render_input(self, varprefix, value):
4269 html.help(self.help())
4270 html.text_input(
4271 varprefix,
4272 default_value=json.dumps(_encode_labels_for_tagify(value.items())).decode("utf-8"),
4273 cssclass="labels",
4274 attrs={
4275 "placeholder": _("Add some label"),
4276 "data-world": self._world.value,
4280 @page_registry.register_page("ajax_autocomplete_labels")
4281 class PageAutocompleteLabels(AjaxPage):
4282 """Return all known labels to support tagify label input dropdown completion"""
4284 def page(self):
4285 request = html.get_request()
4286 return _encode_labels_for_tagify(
4287 self._get_labels(Labels.World(request["world"]), request["search_label"]))
4289 def _get_labels(self, world, search_label):
4290 if world == Labels.World.CONFIG:
4291 return self._get_labels_from_config(search_label)
4293 if world == Labels.World.CORE:
4294 return self._get_labels_from_core(search_label)
4296 raise NotImplementedError()
4298 def _get_labels_from_config(self, search_label):
4299 return [] # TODO: Implement me
4301 # TODO: Provide information about the label source
4302 # Would be better to optimize this kind of query somehow. The best we can
4303 # do without extending livestatus is to use the Cache header for liveproxyd
4304 def _get_labels_from_core(self, search_label):
4305 import cmk.gui.sites as sites
4306 query = ("GET services\n" \
4307 "Cache: reload\n" \
4308 "Columns: host_labels labels\n")
4310 labels = set()
4311 for row in sites.live().query(query):
4312 labels.update(row[0].items())
4313 labels.update(row[1].items())
4315 return list(labels)
4318 def _encode_labels_for_tagify(labels):
4319 return [{"value": "%s:%s" % e} for e in labels]
4322 class IconSelector(ValueSpec):
4323 def __init__(self, **kwargs):
4324 ValueSpec.__init__(self, **kwargs)
4325 self._allow_empty = kwargs.get('allow_empty', True)
4326 self._empty_img = kwargs.get('emtpy_img', 'empty')
4328 self._exclude = [
4329 'trans',
4330 'empty',
4333 @classmethod
4334 def categories(cls):
4335 import cmk.gui.config as config # FIXME: Clean this up. But how?
4336 return config.wato_icon_categories
4338 @classmethod
4339 def category_alias(cls, category_name):
4340 return dict(cls.categories()).get(category_name, category_name)
4342 # All icons within the images/icons directory have the ident of a category
4343 # witten in the PNG meta data. For the default images we have done this scripted.
4344 # During upload of user specific icons, the meta data is added to the images.
4345 def available_icons(self, only_local=False):
4346 dirs = [
4347 os.path.join(cmk.utils.paths.omd_root, "local/share/check_mk/web/htdocs/images/icons"),
4349 if not only_local:
4350 dirs.append(
4351 os.path.join(cmk.utils.paths.omd_root, "share/check_mk/web/htdocs/images/icons"))
4353 valid_categories = dict(self.categories()).keys()
4356 # Read all icons from the icon directories
4358 icons = {}
4359 for directory in dirs:
4360 try:
4361 files = os.listdir(directory)
4362 except OSError:
4363 continue
4365 for file_name in files:
4366 file_path = directory + "/" + file_name
4367 if file_name[-4:] == '.png' and os.path.isfile(file_path):
4369 # extract the category from the meta data
4370 try:
4371 im = Image.open(file_path)
4372 except IOError as e:
4373 if "%s" % e == "cannot identify image file":
4374 continue # Silently skip invalid files
4375 else:
4376 raise
4378 category = im.info.get('Comment')
4379 if category not in valid_categories:
4380 category = 'misc'
4382 icon_name = file_name[:-4]
4383 icons[icon_name] = category
4385 for exclude in self._exclude:
4386 try:
4387 del icons[exclude]
4388 except KeyError:
4389 pass
4391 return icons
4393 def available_icons_by_category(self, icons):
4394 by_cat = {}
4395 for icon_name, category_name in icons.items():
4396 by_cat.setdefault(category_name, [])
4397 by_cat[category_name].append(icon_name)
4399 icon_categories = []
4400 for category_name, category_alias in self.categories():
4401 if category_name in by_cat:
4402 icon_categories.append((category_name, category_alias, by_cat[category_name]))
4403 return icon_categories
4405 def render_icon(self, icon_name, onclick='', title='', id_=''):
4406 if not icon_name:
4407 icon_name = self._empty_img
4409 icon = html.render_icon(icon_name, title=title, middle=True, id_=id_)
4410 if onclick:
4411 icon = html.render_a(icon, href="javascript:void(0)", onclick=onclick)
4413 return icon
4415 def render_input(self, varprefix, value):
4416 # Handle complain phase with validation errors correctly and get the value
4417 # from the HTML vars
4418 if value is None:
4419 value = html.request.var(varprefix + "_value")
4421 self.classtype_info()
4422 if not value:
4423 value = self._empty_img
4425 html.hidden_field(varprefix + "_value", value or '', varprefix + "_value", add_var=True)
4427 if value:
4428 content = self.render_icon(value, '', _('Choose another Icon'), id_=varprefix + '_img')
4429 else:
4430 content = _('Select an Icon')
4432 html.popup_trigger(
4433 content,
4434 varprefix + '_icon_selector',
4435 'icon_selector',
4436 url_vars=[
4437 ('value', value),
4438 ('varprefix', varprefix),
4439 ('allow_empty', '1' if self._allow_empty else '0'),
4440 ('back', html.makeuri([])),
4442 resizable=True,
4445 def render_popup_input(self, varprefix, value):
4446 html.open_div(class_="icons", id_="%s_icons" % varprefix)
4448 icons = self.available_icons()
4449 available_icons = self.available_icons_by_category(icons)
4450 active_category = icons.get(value, available_icons[0][0])
4452 # Render tab navigation
4453 html.open_ul()
4454 for category_name, category_alias, icons in available_icons:
4455 html.open_li(class_="active" if active_category == category_name else None)
4456 # TODO: TEST
4457 html.a(
4458 category_alias,
4459 href="javascript:cmk.valuespecs.iconselector_toggle(\'%s\', \'%s\')" %
4460 (varprefix, category_name),
4461 id_="%s_%s_nav" % (varprefix, category_name),
4462 class_="%s_nav" % varprefix)
4463 html.close_li()
4464 html.close_ul()
4466 # Now render the icons grouped by category
4467 empty = ['empty'] if self._allow_empty else []
4468 for category_name, category_alias, icons in available_icons:
4469 html.open_div(
4470 id_="%s_%s_container" % (varprefix, category_name),
4471 class_=["icon_container", "%s_container" % varprefix],
4472 style="display:none;" if active_category != category_name else None)
4474 for icon in empty + sorted(icons):
4475 html.open_a(
4476 href=None,
4477 class_="icon",
4478 onclick='cmk.valuespecs.iconselector_select(event, \'%s\', \'%s\')' %
4479 (varprefix, icon),
4480 title=icon,
4483 html.write_html(self.render_icon(icon, id_=varprefix + '_i_' + icon, title=icon))
4485 html.span(icon)
4487 html.close_a()
4489 html.close_div()
4491 html.open_div(class_="buttons")
4493 html.jsbutton(
4494 "_toggle_names",
4495 _("Toggle names"),
4496 onclick="cmk.valuespecs.iconselector_toggle_names(event, %s)" % json.dumps(varprefix))
4498 import cmk.gui.config as config # FIXME: Clean this up. But how?
4499 if config.user.may('wato.icons'):
4500 back_param = '&back=' + html.urlencode(
4501 html.get_url_input('back')) if html.request.has_var('back') else ''
4502 html.buttonlink('wato.py?mode=icons' + back_param, _('Manage'))
4504 html.close_div()
4506 html.close_div()
4508 def from_html_vars(self, varprefix):
4509 icon = html.request.var(varprefix + '_value')
4510 if icon == 'empty':
4511 return None
4512 return icon
4514 def value_to_text(self, value):
4515 # TODO: This is a workaround for a bug. This function needs to return str objects right now.
4516 return "%s" % self.render_icon(value)
4518 def validate_datatype(self, value, varprefix):
4519 if value is not None and not isinstance(value, str):
4520 raise MKUserError(varprefix, _("The type is %s, but should be str") % type(value))
4522 def validate_value(self, value, varprefix):
4523 if not self._allow_empty and not value:
4524 raise MKUserError(varprefix, _("You need to select an icon."))
4526 if value and value not in self.available_icons():
4527 raise MKUserError(varprefix, _("The selected icon image does not exist."))
4530 class ListOfTimeRanges(ListOf):
4531 def __init__(self, **kwargs):
4532 super(ListOfTimeRanges, self).__init__(
4533 TimeofdayRange(
4534 allow_empty=True,
4535 allow_24_00=True,
4537 movable=False,
4538 add_label=_("Add time range"),
4539 del_label=_("Delete time range"),
4540 style=ListOf.Style.FLOATING,
4541 magic="#!#",
4542 **kwargs)
4545 # Kept for compatibility reasons (removed in 1.6)
4546 TimeofdayRanges = ListOfTimeRanges
4549 class Fontsize(Float):
4550 def __init__(self, **kwargs):
4551 kwargs.setdefault("title", _("Font size"))
4552 kwargs.setdefault("default_value", 10)
4553 kwargs["size"] = 5
4554 kwargs["unit"] = _("pt")
4555 super(Fontsize, self).__init__(**kwargs)
4558 class Color(ValueSpec):
4559 def __init__(self, **kwargs):
4560 kwargs["regex"] = "#[0-9]{3,6}"
4561 kwargs["regex_error"] = _("The color needs to be given in hex format.")
4562 ValueSpec.__init__(self, **kwargs)
4563 self._on_change = kwargs.get("on_change")
4564 self._allow_empty = kwargs.get("allow_empty", True)
4566 def render_input(self, varprefix, value):
4567 self.classtype_info()
4568 if not value:
4569 value = "#FFFFFF"
4571 # Holds the actual value for form submission
4572 html.hidden_field(varprefix + "_value", value or '', varprefix + "_value", add_var=True)
4574 indicator = html.render_div(
4576 id_="%s_preview" % varprefix,
4577 class_="cp-preview",
4578 style="background-color:%s" % value)
4580 # TODO(rh): Please take a look at this hard coded HTML
4581 # FIXME: Rendering with HTML class causes bug in html popup_trigger function.
4582 # Reason is HTML class and the escaping.
4583 menu_content = "<div id=\"%s_picker\" class=\"cp-small\"></div>" % varprefix
4584 menu_content += "<div class=\"cp-input\">" \
4585 "%s" \
4586 "<input id=\"%s_input\" type=\"text\"></input></div>" % \
4587 (_("Hex color:"), varprefix)
4589 menu_content += "<script language=\"javascript\">" \
4590 "cmk.valuespecs.add_color_picker(%s, %s)" \
4591 "</script>" % (json.dumps(varprefix), json.dumps(value))
4593 html.popup_trigger(
4594 indicator,
4595 varprefix + '_popup',
4596 menu_content=menu_content,
4597 cssclass="colorpicker",
4598 onclose=self._on_change)
4600 def from_html_vars(self, varprefix):
4601 color = html.request.var(varprefix + '_value')
4602 if color == '':
4603 return None
4604 return color
4606 def value_to_text(self, value):
4607 return value
4609 def validate_datatype(self, value, varprefix):
4610 if value is not None and not isinstance(value, str):
4611 raise MKUserError(varprefix, _("The type is %s, but should be str") % type(value))
4613 def validate_value(self, value, varprefix):
4614 if not self._allow_empty and not value:
4615 raise MKUserError(varprefix, _("You need to select a color."))
4618 class SSHKeyPair(ValueSpec):
4619 def __init__(self, **kwargs):
4620 ValueSpec.__init__(self, **kwargs)
4622 def render_input(self, varprefix, value):
4623 self.classtype_info()
4624 if value:
4625 html.write(_("Fingerprint: %s") % self.value_to_text(value))
4626 html.hidden_field(varprefix, self._encode_key_for_url(value), add_var=True)
4627 else:
4628 html.write(_("Key pair will be generated when you save."))
4630 def value_to_text(self, value):
4631 return self._get_key_fingerprint(value)
4633 def from_html_vars(self, varprefix):
4634 if html.request.has_var(varprefix):
4635 return self._decode_key_from_url(html.request.var(varprefix))
4636 return self._generate_ssh_key(varprefix)
4638 @staticmethod
4639 def _encode_key_for_url(value):
4640 return "|".join(value)
4642 @staticmethod
4643 def _decode_key_from_url(text):
4644 return text.split("|")
4646 @classmethod
4647 def _generate_ssh_key(cls, varprefix):
4648 key = RSA.generate(4096)
4649 private_key = key.exportKey('PEM')
4650 pubkey = key.publickey()
4651 public_key = pubkey.exportKey('OpenSSH')
4652 return (private_key, public_key)
4654 @classmethod
4655 def _get_key_fingerprint(cls, value):
4656 _private_key, public_key = value
4657 key = base64.b64decode(public_key.strip().split()[1].encode('ascii'))
4658 fp_plain = hashlib.md5(key).hexdigest()
4659 return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
4662 class SchedulePeriod(CascadingDropdown):
4663 def __init__(self, from_end=True, **kwargs):
4664 if from_end:
4665 from_end_choice = [
4666 ("month_end", _("At the end of every month at day"),
4667 Integer(minvalue=1, maxvalue=28, unit=_("from the end"))),
4669 else:
4670 from_end_choice = []
4672 CascadingDropdown.__init__(
4673 self,
4674 title=_("Period"),
4675 orientation="horizontal",
4676 choices=[
4677 ("day", _("Every day")),
4678 ("week", _("Every week on..."), Weekday(title=_("Day of the week"))),
4679 ("month_begin", _("At the beginning of every month at day"),
4680 Integer(minvalue=1, maxvalue=28)),
4681 ] + from_end_choice)
4684 class CAorCAChain(UploadOrPasteTextFile):
4685 def __init__(self, **args):
4686 args.setdefault("title", _("Certificate Chain (Root / Intermediate Certificate)"))
4687 args.setdefault("file_title", _("CRT/PEM File"))
4688 UploadOrPasteTextFile.__init__(self, **args)
4690 def from_html_vars(self, varprefix):
4691 value = Alternative.from_html_vars(self, varprefix)
4692 if isinstance(value, tuple):
4693 value = value[2] # FileUpload sends (filename, mime-type, content)
4694 return value
4696 def validate_value(self, value, varprefix):
4697 try:
4698 self.analyse_cert(value)
4699 except Exception:
4700 # FIXME TODO: Cleanup this general exception catcher
4701 raise MKUserError(varprefix, _("Invalid certificate file"))
4703 def analyse_cert(self, value):
4704 from OpenSSL import crypto
4705 cert = crypto.load_certificate(crypto.FILETYPE_PEM, value)
4706 titles = {
4707 "C": _("Country"),
4708 "ST": _("State or Province Name"),
4709 "L": _("Locality Name"),
4710 "O": _("Organization Name"),
4711 "CN": _("Common Name"),
4713 cert_info = {}
4714 for what, x509 in [
4715 ("issuer", cert.get_issuer()),
4716 ("subject", cert.get_subject()),
4718 cert_info[what] = {}
4719 for key, val in x509.get_components():
4720 if key in titles:
4721 cert_info[what][titles[key]] = val.decode("utf8")
4722 return cert_info
4724 def value_to_text(self, value):
4725 cert_info = self.analyse_cert(value)
4726 text = "<table>"
4727 for what, title in [
4728 ("issuer", _("Issuer")),
4729 ("subject", _("Subject")),
4731 text += "<tr><td>%s:</td><td>" % title
4732 for title1, val in sorted(cert_info[what].items()):
4733 text += "%s: %s<br>" % (title1, val)
4734 text += "</tr>"
4735 text += "</table>"
4736 return text
4739 def ListOfCAs(**args):
4740 args.setdefault("title", _("CAs to accept"))
4741 args.setdefault(
4742 "help",
4743 _("Only accepting HTTPS connections with a server which certificate "
4744 "is signed with one of the CAs that are listed here. That way it is guaranteed "
4745 "that it is communicating only with the authentic update server. "
4746 "If you use self signed certificates for you server then enter that certificate "
4747 "here."))
4748 args.setdefault("add_label", _("Add new CA certificate or chain"))
4749 args.setdefault(
4750 "empty_text",
4751 _("You need to enter at least one CA. Otherwise no SSL connection can be made."))
4752 args.setdefault("allow_empty", False)
4753 return ListOf(CAorCAChain(), movable=False, **args)
4756 class SiteChoice(DropdownChoice):
4757 def __init__(self, **kwargs):
4758 kwargs.setdefault("title", _("Site"))
4759 kwargs.setdefault("help", _("Specify the site of your choice"))
4760 kwargs.setdefault("default_value", self._site_default_value)
4761 kwargs.setdefault("invalid_choice_error",
4762 _("The configured site is not known to this site."))
4764 kwargs.update({
4765 "choices": self._site_choices,
4766 "invalid_choice": "complain",
4767 "invalid_choice_title": _("Unknown site (%s)"),
4770 super(SiteChoice, self).__init__(**kwargs)
4772 def _site_default_value(self):
4773 import cmk.gui.config as config
4774 if config.is_wato_slave_site():
4775 return False
4777 default_value = config.site_attribute_default_value()
4778 if default_value:
4779 return default_value
4780 return self.canonical_value()
4782 def _site_choices(self):
4783 import cmk.gui.config as config # FIXME
4784 return config.site_attribute_choices()
4787 class LogLevelChoice(DropdownChoice):
4788 def __init__(self, **kwargs):
4789 kwargs.setdefault("default_value", cmk.utils.log.INFO)
4790 kwargs.update({
4791 "choices": [
4792 (cmk.utils.log.CRITICAL, _("Critical")),
4793 (cmk.utils.log.ERROR, _("Error")),
4794 (cmk.utils.log.WARNING, _("Warning")),
4795 (cmk.utils.log.INFO, _("Informational")),
4796 (cmk.utils.log.VERBOSE, _("Verbose")),
4797 (cmk.utils.log.DEBUG, _("Debug")),
4800 super(LogLevelChoice, self).__init__(**kwargs)