#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015-2015: Alignak team, see AUTHORS.txt file for contributors
#
# This file is part of Alignak.
#
# Alignak is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Alignak is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Alignak. If not, see <http://www.gnu.org/licenses/>.
#
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright (C) 2009-2014:
# Hartmut Goebel, h.goebel@goebel-consult.de
# Nicolas Dupeux, nicolas@dupeux.net
# Gerhard Lausser, gerhard.lausser@consol.de
# Grégory Starck, g.starck@gmail.com
# Frédéric Pégé, frederic.pege@gmail.com
# Sebastien Coavoux, s.coavoux@free.fr
# Olivier Hanesse, olivier.hanesse@gmail.com
# Jean Gabes, naparuba@gmail.com
# Zoran Zaric, zz@zoranzaric.de
# David Gil, david.gil.marcos@gmail.com
# This file is part of Shinken.
#
# Shinken is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Shinken is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Shinken. If not, see <http://www.gnu.org/licenses/>.
"""
This class resolve Macro in commands by looking at the macros list
in Class of elements. It give a property that call be callable or not.
It not callable, it's a simple property and replace the macro with the value
If callable, it's a method that is called to get the value. for example, to
get the number of service in a host, you call a method to get the
len(host.services)
"""
import re
import time
from alignak.borg import Borg
[docs]class MacroResolver(Borg):
"""MacroResolver class is used to resolve macros (in command call). See above for details"""
my_type = 'macroresolver'
# Global macros
macros = {
'TOTALHOSTSUP': '_get_total_hosts_up',
'TOTALHOSTSDOWN': '_get_total_hosts_down',
'TOTALHOSTSUNREACHABLE': '_get_total_hosts_unreachable',
'TOTALHOSTSDOWNUNHANDLED': '_get_total_hosts_unhandled',
'TOTALHOSTSUNREACHABLEUNHANDLED': '_get_total_hosts_unreachable_unhandled',
'TOTALHOSTPROBLEMS': '_get_total_host_problems',
'TOTALHOSTPROBLEMSUNHANDLED': '_get_total_host_problems_unhandled',
'TOTALSERVICESOK': '_get_total_service_ok',
'TOTALSERVICESWARNING': '_get_total_services_warning',
'TOTALSERVICESCRITICAL': '_get_total_services_critical',
'TOTALSERVICESUNKNOWN': '_get_total_services_unknown',
'TOTALSERVICESWARNINGUNHANDLED': '_get_total_services_warning_unhandled',
'TOTALSERVICESCRITICALUNHANDLED': '_get_total_services_critical_unhandled',
'TOTALSERVICESUNKNOWNUNHANDLED': '_get_total_services_unknown_unhandled',
'TOTALSERVICEPROBLEMS': '_get_total_service_problems',
'TOTALSERVICEPROBLEMSUNHANDLED': '_get_total_service_problems_unhandled',
'LONGDATETIME': '_get_long_date_time',
'SHORTDATETIME': '_get_short_date_time',
'DATE': '_get_date',
'TIME': '_get_time',
'TIMET': '_get_timet',
'PROCESSSTARTTIME': '_get_process_start_time',
'EVENTSTARTTIME': '_get_events_start_time',
}
output_macros = [
'HOSTOUTPUT',
'HOSTPERFDATA',
'HOSTACKAUTHOR',
'HOSTACKCOMMENT',
'SERVICEOUTPUT',
'SERVICEPERFDATA',
'SERVICEACKAUTHOR',
'SERVICEACKCOMMENT'
]
[docs] def init(self, conf):
"""Init macroresolver instance with conf.
Must be called once.
:param conf: conf to load
:type conf:
:return: None
"""
# For searching class and elements for ondemand
# we need link to types
self.conf = conf
self.lists_on_demand = []
self.hosts = conf.hosts
# For special void host_name handling...
self.host_class = self.hosts.inner_class
self.lists_on_demand.append(self.hosts)
self.services = conf.services
self.contacts = conf.contacts
self.lists_on_demand.append(self.contacts)
self.hostgroups = conf.hostgroups
self.lists_on_demand.append(self.hostgroups)
self.commands = conf.commands
self.servicegroups = conf.servicegroups
self.lists_on_demand.append(self.servicegroups)
self.contactgroups = conf.contactgroups
self.lists_on_demand.append(self.contactgroups)
self.illegal_macro_output_chars = conf.illegal_macro_output_chars
# Try cache :)
# self.cache = {}
def _get_macros(self, chain):
"""Get all macros of a chain
Cut '$' char and create a dict with the following structure::
{ 'MacroSTR1' : {'val': '', 'type': 'unknown'}
'MacroSTR2' : {'val': '', 'type': 'unknown'}
}
:param chain: chain to parse
:type chain: str
:return: dict with macro parsed as key
:rtype: dict
"""
# if chain in self.cache:
# return self.cache[chain]
regex = re.compile(r'(\$)')
elts = regex.split(chain)
macros = {}
in_macro = False
for elt in elts:
if elt == '$':
in_macro = not in_macro
elif in_macro:
macros[elt] = {'val': '', 'type': 'unknown'}
# self.cache[chain] = macros
if '' in macros:
del macros['']
return macros
def _get_value_from_element(self, elt, prop):
"""Get value from a element's property
the property may be a function to call.
:param elt: element
:type elt: object
:param prop: element property
:type prop: str
:return: getattr(elt, prop) or getattr(elt, prop)() (call)
:rtype: str
"""
try:
value = getattr(elt, prop)
if callable(value):
return unicode(value())
else:
return unicode(value)
except AttributeError, exp:
# Return no value
return ''
except UnicodeError, exp:
if isinstance(value, str):
return unicode(value, 'utf8', errors='ignore')
else:
return ''
def _delete_unwanted_caracters(self, chain):
"""Remove not wanted char from chain
unwanted char are illegal_macro_output_chars attribute
:param chain: chain to remove char from
:type chain: str
:return: chain cleaned
:rtype: str
"""
for char in self.illegal_macro_output_chars:
chain = chain.replace(char, '')
return chain
[docs] def get_env_macros(self, data):
"""Get all environment macros from data
For each object in data ::
* Fetch all macros in object.__class__.macros
* Fetch all customs macros in o.custom
:param data: data to get macro
:type data:
:return: dict with macro name as key and macro value as value
:rtype: dict
"""
env = {}
for obj in data:
cls = obj.__class__
macros = cls.macros
for macro in macros:
if macro.startswith("USER"):
break
prop = macros[macro]
value = self._get_value_from_element(obj, prop)
env['NAGIOS_%s' % macro] = value
if hasattr(obj, 'customs'):
# make NAGIOS__HOSTMACADDR from _MACADDR
for cmacro in obj.customs:
new_env_name = 'NAGIOS__' + obj.__class__.__name__.upper() + cmacro[1:].upper()
env[new_env_name] = obj.customs[cmacro]
return env
[docs] def resolve_simple_macros_in_string(self, c_line, data, args=None):
"""Replace macro in the command line with the real value
:param c_line: command line to modify
:type c_line: str
:param data: objects list, use to look for a specific macro
:type data:
:param args: args given to the command line, used to get "ARGN" macros.
:type args:
:return: command line with '$MACRO$' replaced with values
:rtype: str
"""
# Now we prepare the classes for looking at the class.macros
data.append(self) # For getting global MACROS
if hasattr(self, 'conf'):
data.append(self.conf) # For USERN macros
clss = [d.__class__ for d in data]
# we should do some loops for nested macros
# like $USER1$ hiding like a ninja in a $ARG2$ Macro. And if
# $USER1$ is pointing to $USER34$ etc etc, we should loop
# until we reach the bottom. So the last loop is when we do
# not still have macros :)
still_got_macros = True
nb_loop = 0
while still_got_macros:
nb_loop += 1
# Ok, we want the macros in the command line
macros = self._get_macros(c_line)
# We can get out if we do not have macros this loop
still_got_macros = (len(macros) != 0)
# print "Still go macros:", still_got_macros
# Put in the macros the type of macro for all macros
self._get_type_of_macro(macros, clss)
# Now we get values from elements
for macro in macros:
# If type ARGN, look at ARGN cutting
if macros[macro]['type'] == 'ARGN' and args is not None:
macros[macro]['val'] = self._resolve_argn(macro, args)
macros[macro]['type'] = 'resolved'
# If class, get value from properties
if macros[macro]['type'] == 'class':
cls = macros[macro]['class']
for elt in data:
if elt is not None and elt.__class__ == cls:
prop = cls.macros[macro]
macros[macro]['val'] = self._get_value_from_element(elt, prop)
# Now check if we do not have a 'output' macro. If so, we must
# delete all special characters that can be dangerous
if macro in self.output_macros:
macros[macro]['val'] = \
self._delete_unwanted_caracters(macros[macro]['val'])
if macros[macro]['type'] == 'CUSTOM':
cls_type = macros[macro]['class']
# Beware : only cut the first _HOST value, so the macro name can have it on it..
macro_name = re.split('_' + cls_type, macro, 1)[1].upper()
# Ok, we've got the macro like MAC_ADDRESS for _HOSTMAC_ADDRESS
# Now we get the element in data that have the type HOST
# and we check if it got the custom value
for elt in data:
if elt is not None and elt.__class__.my_type.upper() == cls_type:
if '_' + macro_name in elt.customs:
macros[macro]['val'] = elt.customs['_' + macro_name]
# Then look on the macromodulations, in reserver order, so
# the last to set, will be the firt to have. (yes, don't want to play
# with break and such things sorry...)
mms = getattr(elt, 'macromodulations', [])
for macromod in mms[::-1]:
# Look if the modulation got the value,
# but also if it's currently active
if '_' + macro_name in macromod.customs and macromod.is_active():
macros[macro]['val'] = macromod.customs['_' + macro_name]
if macros[macro]['type'] == 'ONDEMAND':
macros[macro]['val'] = self._resolve_ondemand(macro, data)
# We resolved all we can, now replace the macro in the command call
for macro in macros:
c_line = c_line.replace('$' + macro + '$', macros[macro]['val'])
# A $$ means we want a $, it's not a macro!
# We replace $$ by a big dirty thing to be sure to not misinterpret it
c_line = c_line.replace("$$", "DOUBLEDOLLAR")
if nb_loop > 32: # too much loop, we exit
still_got_macros = False
# We now replace the big dirty token we made by only a simple $
c_line = c_line.replace("DOUBLEDOLLAR", "$")
# print "Retuning c_line", c_line.strip()
return c_line.strip()
[docs] def resolve_command(self, com, data):
"""Resolve command macros with data
:param com: check / event handler or command call object
:type com: object
:param data: objects list, use to look for a specific macro
:type data:
:return: command line with '$MACRO$' replaced with values
:rtype: str
"""
c_line = com.command.command_line
return self.resolve_simple_macros_in_string(c_line, data, args=com.args)
def _get_type_of_macro(self, macros, clss):
r"""Set macros types
Example::
ARG\d -> ARGN,
HOSTBLABLA -> class one and set Host in class)
_HOSTTOTO -> HOST CUSTOM MACRO TOTO
SERVICESTATEID:srv-1:Load$ -> MACRO SERVICESTATEID of the service Load of host srv-1
:param macros: macros list
:type macros: list[str]
:param clss: classes list, used to tag class macros
:type clss:
:return: None
"""
for macro in macros:
# ARGN Macros
if re.match(r'ARG\d', macro):
macros[macro]['type'] = 'ARGN'
continue
# USERN macros
# are managed in the Config class, so no
# need to look that here
elif re.match(r'_HOST\w', macro):
macros[macro]['type'] = 'CUSTOM'
macros[macro]['class'] = 'HOST'
continue
elif re.match(r'_SERVICE\w', macro):
macros[macro]['type'] = 'CUSTOM'
macros[macro]['class'] = 'SERVICE'
# value of macro: re.split('_HOST', '_HOSTMAC_ADDRESS')[1]
continue
elif re.match(r'_CONTACT\w', macro):
macros[macro]['type'] = 'CUSTOM'
macros[macro]['class'] = 'CONTACT'
continue
# On demand macro
elif len(macro.split(':')) > 1:
macros[macro]['type'] = 'ONDEMAND'
continue
# OK, classical macro...
for cls in clss:
if macro in cls.macros:
macros[macro]['type'] = 'class'
macros[macro]['class'] = cls
continue
def _resolve_argn(self, macro, args):
"""Get argument from macro name
ie : $ARG3$ -> args[2]
:param macro: macro to parse
:type macro:
:param args: args given to command line
:type args:
:return: argument at position N-1 in args table (where N is the int parsed)
:rtype: None | str
"""
# first, get the number of args
_id = None
matches = re.search(r'ARG(?P<id>\d+)', macro)
if matches is not None:
_id = int(matches.group('id')) - 1
try:
return args[_id]
except IndexError:
return ''
def _resolve_ondemand(self, macro, data):
"""Get on demand macro value
:param macro: macro to parse
:type macro:
:param data: data to get value from
:type data:
:return: macro value
:rtype: str
"""
# print "\nResolving macro", macro
elts = macro.split(':')
nb_parts = len(elts)
macro_name = elts[0]
# Len 3 == service, 2 = all others types...
if nb_parts == 3:
val = ''
# print "Got a Service on demand asking...", elts
(host_name, service_description) = (elts[1], elts[2])
# host_name can be void, so it's the host in data
# that is important. We use our self.host_class to
# find the host in the data :)
if host_name == '':
for elt in data:
if elt is not None and elt.__class__ == self.host_class:
host_name = elt.host_name
# Ok now we get service
serv = self.services.find_srv_by_name_and_hostname(host_name, service_description)
if serv is not None:
cls = serv.__class__
prop = cls.macros[macro_name]
val = self._get_value_from_element(serv, prop)
# print "Got val:", val
return val
# Ok, service was easy, now hard part
else:
val = ''
elt_name = elts[1]
# Special case: elt_name can be void
# so it's the host where it apply
if elt_name == '':
for elt in data:
if elt is not None and elt.__class__ == self.host_class:
elt_name = elt.host_name
for od_list in self.lists_on_demand:
cls = od_list.inner_class
# We search our type by looking at the macro
if macro_name in cls.macros:
prop = cls.macros[macro_name]
i = od_list.find_by_name(elt_name)
if i is not None:
val = self._get_value_from_element(i, prop)
# Ok we got our value :)
break
return val
return ''
def _get_long_date_time(self):
"""Get long date time
Example : Fri 15 May 11:42:39 CEST 2009
:return: long date local time
:rtype: str
TODO: Should be moved to util
TODO: Should consider timezone
"""
return time.strftime("%a %d %b %H:%M:%S %Z %Y").decode('UTF-8', 'ignore')
def _get_short_date_time(self):
"""Get short date time
Example : 10-13-2000 00:30:28
:return: short date local time
:rtype: str
TODO: Should be moved to util
TODO: Should consider timezone
"""
return time.strftime("%d-%m-%Y %H:%M:%S")
def _get_date(self):
"""Get date
Example : 10-13-2000
:return: local date
:rtype: str
TODO: Should be moved to util
TODO: Should consider timezone
"""
return time.strftime("%d-%m-%Y")
def _get_time(self):
"""Get date time
Example : 00:30:28
:return: date local time
:rtype: str
TODO: Should be moved to util
TODO: Should consider timezone
"""
return time.strftime("%H:%M:%S")
def _get_timet(self):
"""Get epoch time
Example : 1437143291
:return: timestamp
:rtype: str
TODO: Should be moved to util
TODO: Should consider timezone
"""
return str(int(time.time()))
def _tot_hosts_by_state(self, state):
"""Generic function to get the number of host in the specified state
:param state: state to filter on
:type state:
:return: number of host in state *state*
:rtype: int
TODO: Should be moved
"""
return sum(1 for h in self.hosts if h.state == state)
_get_total_hosts_up = lambda s: s._tot_hosts_by_state('UP')
_get_total_hosts_down = lambda s: s._tot_hosts_by_state('DOWN')
_get_total_hosts_unreachable = lambda s: s._tot_hosts_by_state('UNREACHABLE')
def _get_total_hosts_unreachable_unhandled(self):
"""DOES NOTHING( Should get the number of unreachable hosts not handled)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0
def _get_total_hosts_problems(self):
"""Get the number of hosts that are a problem
:return: number of hosts with is_problem attribute True
:rtype: int
"""
return sum(1 for h in self.hosts if h.is_problem)
def _get_total_hosts_problems_unhandled(self):
"""DOES NOTHING( Should get the number of host problems not handled)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0
def _tot_services_by_state(self, state):
"""Generic function to get the number of service in the specified state
:param state: state to filter on
:type state:
:return: number of service in state *state*
:rtype: int
TODO: Should be moved
"""
return sum(1 for s in self.services if s.state == state)
_get_total_service_ok = lambda s: s._tot_services_by_state('OK')
_get_total_service_warning = lambda s: s._tot_services_by_state('WARNING')
_get_total_service_critical = lambda s: s._tot_services_by_state('CRITICAL')
_get_total_service_unknown = lambda s: s._tot_services_by_state('UNKNOWN')
def _get_total_services_warning_unhandled(self):
"""DOES NOTHING (Should get the number of warning services not handled)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0
def _get_total_services_critical_unhandled(self):
"""DOES NOTHING (Should get the number of critical services not handled)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0
def _get_total_services_unknown_unhandled(self):
"""DOES NOTHING (Should get the number of unknown services not handled)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0
def _get_total_service_problems(self):
"""Get the number of services that are a problem
:return: number of services with is_problem attribute True
:rtype: int
"""
return sum(1 for s in self.services if s.is_problem)
def _get_total_service_problems_unhandled(self):
"""DOES NOTHING (Should get the number of service problems not handled)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0
def _get_process_start_time(self):
"""DOES NOTHING ( Should get process start time)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0
def _get_events_start_time(self):
"""DOES NOTHING ( Should get events start time)
:return: 0 always
:rtype: int
TODO: Implement this
"""
return 0