# -*- coding: utf-8 -*-
""" Classes and functions related to Perfdata metrics."""
from __future__ import absolute_import
import shlex
import six
import re
from pynag import errors
from pynag.Plugins import new_threshold_syntax
from pynag.Plugins import classic_threshold_syntax
from six.moves import map
MULTIPLIERS = {
'h': 10**2,
'k': 10**3,
'M': 10**6,
'G': 10**9,
'T': 10**12,
'P': 10**15,
'E': 10**18,
'Z': 10**21,
'Y': 10**24,
'kB': 1000,
'MB': 1000**2,
'GB': 1000**3,
'TB': 1000**4,
'PB': 1000**5,
'EB': 1000**6,
'ZB': 1000**7,
'YB': 1000**8,
'kiB': 1024,
'MiB': 1024**2,
'GiB': 1024**3,
'TiB': 1024**4,
'PiB': 1024**5,
'EiB': 1024**6,
'ZiB': 1024**7,
'YiB': 1024**8,
}
class PerfDataMetric(object):
""" Data structure for one single Nagios Perfdata Metric
Attributes:
perfdatastring (str): Complete perfdata string
label (str): Label section of the perfdata string
value (str): Value section of the perfdata string
warn (str): WARNING threshold
crit (str): CRITICAL threshold
min (str): Minimal value of control
max (str): Maximal value of control
uom (str): Measure unit (octets, bits/s, volts, ...)
"""
label = "" # : str. Label section of the perfdata string
value = "" # : str. Value section of the perfdata string
warn = "" # : str. WARNING threshold
crit = "" # : str CRITICAL threshold
min = "" # : str. Minimal value of control
max = "" # : str. Maximal value of control
uom = "" # : str. Measure unit (octets, bits/s, volts, ...)
def __repr__(self):
return "'%s'=%s%s;%s;%s;%s;%s" % (
self.label,
self.value,
self.uom,
self.warn,
self.crit,
self.min,
self.max,
)
def __str__(self):
return self.__repr__()
def __init__(self, perfdatastring="", label="", value="", warn="", crit="", min="", max="", uom=""):
"""
>>> p = PerfData(perfdatastring="size=10M;20M;;;")
>>> metric = p.get_perfdatametric('size')
>>> print(metric.label)
size
>>> print(metric.value)
10
>>> print(metric.uom)
M
>>> p = PerfDataMetric(perfdatastring="'with spaces'=10")
>>> print(p.label)
with spaces
>>> print(p.value)
10
"""
self.label = label
self.value = value
self.warn = warn
self.crit = crit
self.min = min
self.max = max
self.uom = uom
perfdatastring = str(perfdatastring)
# Hack: For some weird reason livestatus sometimes delivers perfdata in
# utf-32 encoding.
perfdatastring = perfdatastring.replace('\x00', '')
if len(perfdatastring) == 0:
return
# If label is single quoted, there might be any symbol in the label
# including other single quotes and the = sign. Therefore, we take
# special precautions if it is so
if perfdatastring.startswith("'"):
tmp = perfdatastring.split("'")
everything_but_label = tmp.pop()
tmp.pop(0)
label = "'".join(tmp)
else:
# Split into label=perfdata
tmp = perfdatastring.split('=', 1)
# If no = sign, then we just take in a label
if tmp:
label = tmp.pop(0)
if tmp:
everything_but_label = tmp.pop()
else:
everything_but_label = ''
self.label = label
# Next split string into value;warning;critical;min;max
tmp = everything_but_label.split(';')
if len(tmp) > 0:
val = tmp.pop(0).strip('=')
self.value, self.uom = split_value_and_uom(val)
if len(tmp) > 0:
self.warn = tmp.pop(0)
if len(tmp) > 0:
self.crit = tmp.pop(0)
if len(tmp) > 0:
self.min = tmp.pop(0)
if len(tmp) > 0:
self.max = tmp.pop(0)
def get_status(self):
""" Return nagios-style exit code (int 0-3) by comparing
Example:
self.value with self.warn and self.crit
>>> PerfDataMetric("label1=10;20;30").get_status()
0
>>> PerfDataMetric("label2=25;20;30").get_status()
1
>>> PerfDataMetric("label3=35;20;30").get_status()
2
Invalid metrics always return unknown
>>> PerfDataMetric("label3=35;invalid_metric").get_status()
3
"""
try:
status = classic_threshold_syntax.check_threshold(self.value, warning=self.warn, critical=self.crit)
except errors.PynagError:
status = 3
return status
def is_valid(self):
""" Returns True if all Performance data is valid. Otherwise False
Example Usage:
>>> PerfDataMetric("load1=2").is_valid()
True
>>> PerfDataMetric("load1").is_valid()
False
>>> PerfDataMetric('').is_valid()
False
>>> PerfDataMetric('invalid_value=invalid').is_valid()
False
>>> PerfDataMetric('invalid_min=0;0;0;min;0').is_valid()
False
>>> PerfDataMetric('invalid_min=0;0;0;0;max').is_valid()
False
>>> PerfDataMetric('label with spaces=0').is_valid()
False
>>> PerfDataMetric("'label with spaces=0'").is_valid()
False
>>> PerfDataMetric("value=5.5").is_valid()
True
>>> PerfDataMetric("value=5,5").is_valid()
True
"""
if self.label in (None, ''):
return False
if self.value in (None, ''):
return False
try:
float(self.value)
except ValueError:
return False
try:
self.min == '' or float(self.min)
except ValueError:
return False
try:
self.max == '' or float(self.max)
except ValueError:
return False
if self.label.find(' ') > -1 and not self.label.startswith("'") and not self.label.endswith("'"):
return False
# If we get here, we passed all tests
return True
def reconsile_thresholds(self):
""" Convert threshold from new threshold syntax to classic.
For backwards compatibility
Example:
>>> p = PerfDataMetric(warn='0..100')
>>> p.warn
'0..100'
>>> p.reconsile_thresholds()
>>> p.warn == six.u('@0:100')
True
"""
self.warn = new_threshold_syntax.convert_to_classic_format(self.warn)
self.crit = new_threshold_syntax.convert_to_classic_format(self.crit)
def get_dict(self):
""" Returns a dictionary which contents this class' attributes.
Returns:
Dict. With every key as a string, and every value is a string.
{
'label': self.label,
'value': self.value,
'uom': self.uom,
'warn': self.warn,
'crit': self.crit,
'min': self.min,
'max': self.max,
}
Examples:
>>> p = PerfDataMetric("load=5")
>>> p.get_dict()['min']
''
>>> p.get_dict()['max']
''
>>> p.get_dict()['value']
'5'
>>> p.get_dict()['label']
'load'
>>> p.get_dict()['warn']
''
>>> p.get_dict()['crit']
''
>>> p.get_dict()['uom']
''
"""
return {
'label': self.label,
'value': self.value,
'uom': self.uom,
'warn': self.warn,
'crit': self.crit,
'min': self.min,
'max': self.max,
}
def get_base_value(self):
"""Get the base value for current metric.
This is a simple convenience wrapper around get_base_value()
module function.
Returns:
float. Base value of self.value after unit of measurement
has been taken into account.
Examples:
>>> p = PerfDataMetric('size=10KiB')
>>> p.get_base_value()
10240.0
"""
return get_base_value(self.value, self.uom, self.max)
class PerfData(object):
""" Data Structure for a nagios perfdata string with multiple perfdata metric
Example string:
>>> perf = PerfData("load1=10 load2=10 load3=20 'label with spaces'=5")
>>> perf.metrics
['load1'=10;;;;, 'load2'=10;;;;, 'load3'=20;;;;, 'label with spaces'=5;;;;]
>>> for i in perf.metrics: print("%s %s" % (i.label, i.value))
load1 10
load2 10
load3 20
label with spaces 5
"""
def __init__(self, perfdatastring=""):
"""
>>> perf = PerfData("load1=10 load2=10 load3=20")
"""
self.metrics = []
self.invalid_metrics = []
# Hack: For some weird reason livestatus sometimes delivers perfdata in
# utf-32 encoding.
perfdatastring = perfdatastring.replace('\x00', '')
try:
perfdata = shlex.split(perfdatastring)
for metric in perfdata:
try:
self.add_perfdatametric(metric)
except Exception:
self.invalid_metrics.append(metric)
except ValueError:
return
def is_valid(self):
""" Returns True if the every metric in the string is valid
Example usage:
>>> PerfData("load1=10 load2=10 load3=20").is_valid()
True
>>> PerfData("10b").is_valid()
False
>>> PerfData("load1=").is_valid()
False
>>> PerfData("load1=10 10").is_valid()
False
"""
for i in self.metrics:
if not i.is_valid():
return False
# If we get here, all tests passed
return True
def add_perfdatametric(self, perfdatastring="", label="", value="", warn="", crit="", min="", max="", uom=""):
""" Add a new perfdatametric to existing list of metrics.
Args:
perfdatastring (str): Complete perfdata string
label (str): Label section of the perfdata string
value (str): Value section of the perfdata string
warn (str): WARNING threshold
crit (str): CRITICAL threshold
min (str): Minimal value of control
max (str): Maximal value of control
uom (str): Measure unit (octets, bits/s, volts, ...)
Example:
>>> s = PerfData()
>>> s.add_perfdatametric("a=1")
>>> s.add_perfdatametric(label="utilization",value="10",uom="%")
"""
metric = PerfDataMetric(perfdatastring=perfdatastring, label=label, value=value, warn=warn, crit=crit, min=min, max=max, uom=uom)
self.metrics.append(metric)
def get_perfdatametric(self, metric_name):
""" Get one specific perfdatametric
Args:
metric_name (str): Name of the metric to return
Example:
>>> s = PerfData("cpu=90% memory=50% disk_usage=20%")
>>> my_metric = s.get_perfdatametric('cpu')
>>> my_metric.label, my_metric.value
('cpu', '90')
"""
for i in self.metrics:
if i.label == metric_name:
return i
def reconsile_thresholds(self):
"""Convert all warn and crit thresholds into classic thresholds format.
Example:
>>> p = PerfData('load=15;0..5;;;')
>>> print(p)
'load'=15;0..5;;;
>>> p.reconsile_thresholds()
>>> print(p)
'load'=15;@0:5;;;
"""
for i in self.metrics:
i.reconsile_thresholds()
def __str__(self):
""" Simple string representation of our PerfData.
Example:
>>> p = PerfData('load=15')
>>> str(p)
"'load'=15;;;;"
"""
metrics = [x.__str__() for x in self.metrics]
return ' '.join(metrics)
def split_value_and_uom(value):
"""split_value_and_uom("10mb") -> ('10', 'mb')
Args:
value: String. Usually a perfdata metric like '10mb'
Returns:
A tuple of ('str', 'str') e.g. ('10', 'mb')
Examples:
>>> split_value_and_uom( "10" )
('10', '')
>>> split_value_and_uom( "10c" )
('10', 'c')
>>> split_value_and_uom( "10B" )
('10', 'B')
>>> split_value_and_uom( "10MB" )
('10', 'MB')
>>> split_value_and_uom( "10KB" )
('10', 'KB')
>>> split_value_and_uom( "10TB" )
('10', 'TB')
>>> split_value_and_uom( "10%" )
('10', '%')
>>> split_value_and_uom( "10s" )
('10', 's')
>>> split_value_and_uom( "10us" )
('10', 'us')
>>> split_value_and_uom( "10ms" )
('10', 'ms')
"""
tmp = re.findall(r"([-]*[\d.]*\d+)(.*)", value)
if len(tmp) == 0:
return '', ''
return tmp[0]
def get_base_value(value, uom=None, maximum=None):
""" Get base value of a metric (i.e. turns 1KB into 1024).
Examples:
>>> get_base_value(value=50)
50.0
>>> get_base_value(value=1, uom='kib')
1024.0
>>> get_base_value(value=1, uom='k')
1000.0
>>> get_base_value(value=1, uom='kb')
1000.0
>>> get_base_value(value=1, uom='gib')
1073741824.0
>>> get_base_value(value=1, uom='g')
1000000000.0
>>> get_base_value(value=50, uom='%', maximum=10)
5.0
>>> get_base_value(value=50, uom='%')
Traceback (most recent call last):
...
ValueError: Cant get absolute value for 50% unless max is defined.
>>> get_base_value(value=50, uom='FOO')
Traceback (most recent call last):
...
ValueError: Dont know how to get the base value of a "FOO".
Returns:
float. Base value of self.value after uom has been taken into account.
"""
float_value = float(value)
all_multipliers_in_lowercase = {}
for key, multiplier in MULTIPLIERS.items():
all_multipliers_in_lowercase[key.lower()] = multiplier
if not uom:
return float_value
elif uom == '%' and not maximum:
raise ValueError('Cant get absolute value for %s%s unless max is defined.' % (value, uom))
elif uom == '%':
return float_value * 0.01 * float(maximum)
elif uom in MULTIPLIERS:
return float_value * MULTIPLIERS[uom]
elif uom.lower() in all_multipliers_in_lowercase:
return float_value * all_multipliers_in_lowercase[uom.lower()]
else:
raise ValueError('Dont know how to get the base value of a "%s".' % uom)