Skip to content

Commit

Permalink
CheckAPI: add render module to API
Browse files Browse the repository at this point in the history
CMK-4335

Change-Id: I44740dd2853c7d0955eea9ff7e871c0522d80edb
  • Loading branch information
mo-ki committed Apr 29, 2020
1 parent a74e03d commit 2e3ad0c
Show file tree
Hide file tree
Showing 4 changed files with 341 additions and 1 deletion.
177 changes: 177 additions & 0 deletions cmk/base/api/agent_based/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.
"""Render functions for check development
These are meant to be exposed in the API
"""
from typing import Iterable, Optional, Tuple
import math
import time

_DATE_FORMAT = "%b %d %Y"

_TIME_UNITS = [
('years', 31536000),
('days', 86400),
('hours', 3600),
('minutes', 60),
('seconds', 1),
('milliseconds', 1e-3),
('microseconds', 1e-6),
('nanoseconds', 1e-9),
('picoseconds', 1e-12),
('femtoseconds', 1e-15),
# and while we're at it:
('attoseconds', 1e-18),
('zeptoseconds', 1e-21),
('yoctoseconds', 1e-24),
]

# Karl Marx Gave The Proletariat Eleven Zeppelins, Yo!
_SIZE_PREFIXES = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']


def date(epoch):
# type: (Optional[float]) -> str
"""Render seconds since epoch as date
In case None is given it returns "never".
"""
if epoch is None:
return "never"
return time.strftime(_DATE_FORMAT, time.localtime(float(epoch)))


def datetime(epoch):
# type: (Optional[float]) -> str
"""Render seconds since epoch as date and time
In case None is given it returns "never".
"""
if epoch is None:
return "never"
return time.strftime("%s %%H:%%M:%%S" % _DATE_FORMAT, time.localtime(float(epoch)))


def _gen_timespan_chunks(seconds, nchunks):
# type: (float, int) -> Iterable[str]
try:
start = next(i for i, (_, v) in enumerate(_TIME_UNITS) if seconds >= v)
except StopIteration:
start = len(_TIME_UNITS) - 1

for unit, scale in _TIME_UNITS[start:start + nchunks]:
value = int(seconds / scale)
yield "%.0f %s" % (value, unit if value != 1 else unit[:-1])
if unit.endswith("seconds"):
break
seconds %= scale


def timespan(seconds):
# type: (float) -> str
"""Render a time span in seconds
"""
return " ".join(_gen_timespan_chunks(float(seconds), nchunks=2))


def _digits_left(value):
# type: (float) -> int
"""Return the number of didgits left of the decimal point"""
return max(int(math.log(value, 10) + 1), 1)


def _auto_scale(value, use_si_units):
# type: (float, bool) -> Tuple[str, str]
base = 1000 if use_si_units else 1024
exponent = min(max(int(math.log(value, base)), 0), len(_SIZE_PREFIXES) - 1)
unit = (_SIZE_PREFIXES[exponent] + ("B" if use_si_units else "iB")).lstrip('i')
scaled_value = float(value) / base**exponent
fmt = "%%.%df" % max(3 - _digits_left(scaled_value), 0)
return fmt % scaled_value, unit


def disksize(bytes_):
# type: (float) -> str
"""Render a disk size in bytes using an appropriate SI prefix
Example:
>>> disksize(1024)
"1.02 KB"
"""
value_str, unit = _auto_scale(float(bytes_), use_si_units=True)
return "%s %s" % (value_str if unit != "B" else value_str.split('.')[0], unit)


def bytes(bytes_): # pylint: disable=redefined-builtin
# type: (float) -> str
"""Render a number of bytes using an appropriate IEC prefix
Example:
>>> bytes(1024**2)
"2.00 MiB"
"""
value_str, unit = _auto_scale(float(bytes_), use_si_units=False)
return "%s %s" % (value_str if unit != "B" else value_str.split('.')[0], unit)


def filesize(bytes_):
# type: (float) -> str
"""Render a file size in bytes
Example:
>>> filesize(12345678)
"12,345,678"
"""
val_str = "%.0f" % float(bytes_)
if len(val_str) <= 3:
return "%s B" % val_str

offset = len(val_str) % 3
groups = [val_str[0:offset]] + [val_str[i:i + 3] for i in range(offset, len(val_str), 3)]
return "%s B" % ','.join(groups)


def networkbandwidth(octets_per_sec):
# type: (float) -> str
"""Render network bandwidth using an appropriate SI prefix"""
return "%s %sit/s" % _auto_scale(float(octets_per_sec) * 8, use_si_units=True)


def nicspeed(octets_per_sec):
# type: (float) -> str
"""Render NIC speed using an appropriate SI prefix"""
value_str, unit = _auto_scale(float(octets_per_sec) * 8, use_si_units=True)
if '.' in value_str:
value_str = value_str.rstrip("0").rstrip(".")
return "%s %sit/s" % (value_str, unit)


def iobandwidth(bytes_):
# type: (float) -> str
"""Render IO-bandwith using an appropriate SI prefix"""
return "%s %s/s" % _auto_scale(float(bytes_), use_si_units=True)


def percent(percentage):
# type: (float) -> str
"""Render percentage"""
# There is another render.percent in cmk.utils. However, that deals extensively with
# the rendering of small percentages (as is required for graphing applications)
# However, we assume that if a percentage value is smaller that 0.01%, we can display
# it as 0.00%.
# If this is the case regularly, you probably want to display something different,
# "parts per million" for instance.
# Also, this way this module is completely stand alone.
value = float(percentage) # be nice
if not value:
return "0%"
digits_int = _digits_left(value)
digits_frac = max(3 - digits_int, 0)
if 99 < value < 100: # be more precise in this case
digits_frac = _digits_left(1. / (100.0 - value))
fmt = "%%.%df%%%%" % digits_frac
return fmt % value
3 changes: 2 additions & 1 deletion cmk/base/plugins/agent_based/v0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from cmk.base.discovered_labels import HostLabel
from cmk.base.snmp_utils import OIDCached, OIDBytes

from . import register, state
from . import register, render, state

__all__ = [
# register functions
Expand All @@ -45,6 +45,7 @@
"OIDBytes",
"HostLabel",
# utils
"render",
"parse_string_table",
# detect spec helper
"all_of",
Expand Down
27 changes: 27 additions & 0 deletions cmk/base/plugins/agent_based/v0/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.
"""The "render" namespace adds functions to render values in a human readable way.
All of the render functions take a single numerical value as an argument, and return
a string.
"""
from cmk.base.api.agent_based.render import ( # pylint: disable=redefined-builtin
date, datetime, timespan, disksize, bytes, filesize, networkbandwidth, nicspeed, iobandwidth,
percent,
)

__all__ = [
'date',
'datetime',
'timespan',
'disksize',
'bytes',
'filesize',
'networkbandwidth',
'nicspeed',
'iobandwidth',
'percent',
]
135 changes: 135 additions & 0 deletions tests-py3/unit/cmk/base/api/agent_based/test_render_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.
import pytest # type: ignore[import]

import cmk.base.api.agent_based.render as render


@pytest.mark.parametrize("epoch, output", [
(0, "Jan 01 1970"),
(1587901020, "Apr 26 2020"),
(1587901020.0, "Apr 26 2020"),
("1587901020", "Apr 26 2020"),
])
def test_date(epoch, output):
assert render.date(epoch=epoch) == output


@pytest.mark.parametrize("epoch, output", [
(0, "Jan 01 1970 01:00:00"),
(1587901020, "Apr 26 2020 13:37:00"),
(1587901020.0, "Apr 26 2020 13:37:00"),
("1587901020", "Apr 26 2020 13:37:00"),
])
def test_datetime(epoch, output):
assert render.datetime(epoch=epoch) == output


@pytest.mark.parametrize("seconds, output", [
(0.00000001, "10 nanoseconds"),
(0.1, "100 milliseconds"),
(22, "22 seconds"),
(158, "2 minutes 38 seconds"),
(98, "1 minute 38 seconds"),
(1234567, "14 days 6 hours"),
(31536001, "1 year 0 days"),
])
def test_timespan(seconds, output):
assert render.timespan(seconds=seconds) == output


@pytest.mark.parametrize("bytes_, output", [
(1, "1 B"),
(2345, "2.35 KB"),
(1024**2, "1.05 MB"),
(1000**2, "1.00 MB"),
(1234000, "1.23 MB"),
(12340006, "12.3 MB"),
(123400067, "123 MB"),
(1234000678, "1.23 GB"),
])
def test_disksize(bytes_, output):
assert render.disksize(bytes_) == output


@pytest.mark.parametrize("bytes_, output", [
(1, "1 B"),
(2345, "2.29 KiB"),
(1024**2, "1.00 MiB"),
(1000**2, "977 KiB"),
(1234000, "1.18 MiB"),
(12340006, "11.8 MiB"),
(123400067, "118 MiB"),
(1234000678, "1.15 GiB"),
])
def test_bytes(bytes_, output):
assert render.bytes(bytes_) == output


@pytest.mark.parametrize("bytes_, output", [
(1, "1 B"),
(2345, "2,345 B"),
(1024**2, "1,048,576 B"),
(1000**2, "1,000,000 B"),
(1234000678, "1,234,000,678 B"),
])
def test_filesize(bytes_, output):
assert render.filesize(bytes_) == output


@pytest.mark.parametrize("octets_per_sec, output", [
(1, "8 Bit/s"),
(2345, "18.8 KBit/s"),
(1.25 * 10**5, "1 MBit/s"),
(1.25 * 10**6, "10 MBit/s"),
(1.25 * 10**7, "100 MBit/s"),
(1234000678, "9.87 GBit/s"),
])
def test_nicspeed(octets_per_sec, output):
assert render.nicspeed(octets_per_sec) == output


@pytest.mark.parametrize("octets_per_sec, output", [
(1, "8.00 Bit/s"),
(2345, "18.8 KBit/s"),
(1.25 * 10**5, "1.00 MBit/s"),
(1.25 * 10**6, "10.0 MBit/s"),
(1.25 * 10**7, "100 MBit/s"),
(1234000678, "9.87 GBit/s"),
])
def test_networkbandwitdh(octets_per_sec, output):
assert render.networkbandwidth(octets_per_sec) == output


@pytest.mark.parametrize("bytes_, output", [
(1, "1.00 B/s"),
(2345, "2.35 KB/s"),
(1024**2, "1.05 MB/s"),
(1000**2, "1.00 MB/s"),
(1234000678, "1.23 GB/s"),
])
def test_iobandwidth(bytes_, output):
assert render.iobandwidth(bytes_) == output


@pytest.mark.parametrize("percentage, output", [
(0., "0%"),
(0.001, "0.00%"),
(0.01, "0.01%"),
(1.0, "1.00%"),
(10, "10.0%"),
(99.8, "99.8%"),
(99.9, "99.90%"),
(99.92, "99.92%"),
(99.9991, "99.9991%"),
(99.9997, "99.9997%"),
(100, "100%"),
(100.01, "100%"),
(100, "100%"),
(123, "123%"),
])
def test_percent(percentage, output):
assert render.percent(percentage) == output

0 comments on commit 2e3ad0c

Please sign in to comment.