#!/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
# Sebastien Coavoux, s.coavoux@free.fr
# Guillaume Bour, guillaume@bour.cc
# aviau, alexandre.viau@savoirfairelinux.com
# Nicolas Dupeux, nicolas@dupeux.net
# Grégory Starck, g.starck@gmail.com
# Gerhard Lausser, gerhard.lausser@consol.de
# Andrew McGilvray, amcgilvray@kixeye.com
# Christophe Simon, geektophe@gmail.com
# Jean Gabes, naparuba@gmail.com
# Romain Forlot, rforlot@yahoo.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 module provides Escalation and Escalations classes that
implements escalation for notification. Basically used for parsing.
"""
from alignak.objects.item import Item, Items
from alignak.util import strip_and_uniq
from alignak.property import BoolProp, IntegerProp, StringProp, ListProp
from alignak.log import logger
[docs]class Escalation(Item):
"""Escalation class is used to implement notification escalation
"""
_id = 1 # zero is always special in database, so we do not take risk here
my_type = 'escalation'
properties = Item.properties.copy()
properties.update({
'escalation_name': StringProp(),
'first_notification': IntegerProp(),
'last_notification': IntegerProp(),
'first_notification_time': IntegerProp(),
'last_notification_time': IntegerProp(),
# by default don't use the notification_interval defined in
# the escalation, but the one defined by the object
'notification_interval': IntegerProp(default=-1),
'escalation_period': StringProp(default=''),
'escalation_options': ListProp(default=['d', 'u', 'r', 'w', 'c'], split_on_coma=True),
'contacts': ListProp(default=[], split_on_coma=True),
'contact_groups': ListProp(default=[], split_on_coma=True),
})
running_properties = Item.running_properties.copy()
running_properties.update({
'time_based': BoolProp(default=False),
})
special_properties = ('contacts', 'contact_groups',
'first_notification_time', 'last_notification_time')
special_properties_time_based = ('contacts', 'contact_groups',
'first_notification', 'last_notification')
[docs] def get_name(self):
"""Accessor to escalation_name attribute
:return: escalation name
:rtype: str
"""
return self.escalation_name
[docs] def is_eligible(self, timestamp, status, notif_number, in_notif_time, interval):
"""Check if the escalation is eligible (notification is escalated or not)
Escalation is NOT eligible in ONE of the following condition is fulfilled::
* escalation is not time based and notification number not in range
[first_notification;last_notification] (if last_notif == 0, it's infinity)
* escalation is time based and notification time not in range
[first_notification_time;last_notification_time] (if last_notif_time == 0, it's infinity)
* status does not matches escalation_options ('WARNING' <=> 'w' ...)
* escalation_period is not legit for this time (now usually)
:param timestamp: timestamp to check if timeperiod is valid
:type timestamp: int
:param status: item status (one of the small_states key)
:type status: str
:param notif_number: current notification number
:type notif_number: int
:param in_notif_time: current notification time
:type in_notif_time: int
:param interval: time interval length
:type interval: int
:return: True if no condition has been fulfilled, otherwise False
:rtype: bool
"""
small_states = {
'WARNING': 'w', 'UNKNOWN': 'u', 'CRITICAL': 'c',
'RECOVERY': 'r', 'FLAPPING': 'f', 'DOWNTIME': 's',
'DOWN': 'd', 'UNREACHABLE': 'u', 'OK': 'o', 'UP': 'o'
}
# If we are not time based, we check notification numbers:
if not self.time_based:
# Begin with the easy cases
if notif_number < self.first_notification:
return False
# self.last_notification = 0 mean no end
if self.last_notification != 0 and notif_number > self.last_notification:
return False
# Else we are time based, we must check for the good value
else:
# Begin with the easy cases
if in_notif_time < self.first_notification_time * interval:
return False
# self.last_notification = 0 mean no end
if self.last_notification_time != 0 and \
in_notif_time > self.last_notification_time * interval:
return False
# If our status is not good, we bail out too
if status in small_states and small_states[status] not in self.escalation_options:
return False
# Maybe the time is not in our escalation_period
if self.escalation_period is not None and \
not self.escalation_period.is_time_valid(timestamp):
return False
# Ok, I do not see why not escalade. So it's True :)
return True
[docs] def get_next_notif_time(self, t_wished, status, creation_time, interval):
"""Get the next notification time for the escalation
Only legit for time based escalation
:param t_wished: time we would like to send a new notification (usually now)
:type t_wished:
:param status: status of the host or service
:type status:
:param creation_time: time the notification was created
:type creation_time:
:param interval: time interval length
:type interval: int
:return: timestamp for next notification or None
:rtype: int | None
"""
small_states = {'WARNING': 'w', 'UNKNOWN': 'u', 'CRITICAL': 'c',
'RECOVERY': 'r', 'FLAPPING': 'f', 'DOWNTIME': 's',
'DOWN': 'd', 'UNREACHABLE': 'u', 'OK': 'o', 'UP': 'o'}
# If we are not time based, we bail out!
if not self.time_based:
return None
# Check if we are valid
if status in small_states and small_states[status] not in self.escalation_options:
return None
# Look for the min of our future validity
start = self.first_notification_time * interval + creation_time
# If we are after the classic next time, we are not asking for a smaller interval
if start > t_wished:
return None
# Maybe the time we found is not a valid one....
if self.escalation_period is not None and not self.escalation_period.is_time_valid(start):
return None
# Ok so I ask for my start as a possibility for the next notification time
return start
[docs] def is_correct(self):
"""Check if all elements got a good configuration
:return: True if the configuration is correct, otherwise False
:rtype: bool
"""
state = True
cls = self.__class__
# If we got the _time parameters, we are time based. Unless, we are not :)
if hasattr(self, 'first_notification_time') or hasattr(self, 'last_notification_time'):
self.time_based = True
special_properties = self.special_properties_time_based
else: # classic ones
special_properties = self.special_properties
for prop, entry in cls.properties.items():
if prop not in special_properties:
if not hasattr(self, prop) and entry.required:
logger.info('%s: I do not have %s', self.get_name(), prop)
state = False # Bad boy...
# Raised all previously saw errors like unknown contacts and co
if self.configuration_errors != []:
state = False
for err in self.configuration_errors:
logger.info(err)
# Ok now we manage special cases...
if not hasattr(self, 'contacts') and not hasattr(self, 'contact_groups'):
logger.info('%s: I do not have contacts nor contact_groups', self.get_name())
state = False
# If time_based or not, we do not check all properties
if self.time_based:
if not hasattr(self, 'first_notification_time'):
logger.info('%s: I do not have first_notification_time', self.get_name())
state = False
if not hasattr(self, 'last_notification_time'):
logger.info('%s: I do not have last_notification_time', self.get_name())
state = False
else: # we check classical properties
if not hasattr(self, 'first_notification'):
logger.info('%s: I do not have first_notification', self.get_name())
state = False
if not hasattr(self, 'last_notification'):
logger.info('%s: I do not have last_notification', self.get_name())
state = False
return state
[docs]class Escalations(Items):
"""Escalations manage a list of Escalation objects, used for parsing configuration
"""
name_property = "escalation_name"
inner_class = Escalation
[docs] def linkify(self, timeperiods, contacts, services, hosts):
"""Create link between objects::
* escalation -> host
* escalation -> service
* escalation -> timeperiods
* escalation -> contact
:param timeperiods: timeperiods to link
:type timeperiods: alignak.objects.timeperiod.Timeperiods
:param contacts: contacts to link
:type contacts: alignak.objects.contact.Contacts
:param services: services to link
:type services: alignak.objects.service.Services
:param hosts: hosts to link
:type hosts: alignak.objects.host.Hosts
:return: None
"""
self.linkify_with_timeperiods(timeperiods, 'escalation_period')
self.linkify_with_contacts(contacts)
self.linkify_es_by_s(services)
self.linkify_es_by_h(hosts)
[docs] def add_escalation(self, escalation):
"""Wrapper for add_item method
:param escalation: escalation to add to item dict
:type escalation: alignak.objects.escalation.Escalation
:return: None
"""
self.add_item(escalation)
[docs] def linkify_es_by_s(self, services):
"""Add each escalation object into service.escalation attribute
:param services: service list, used to look for a specific service
:type services: alignak.objects.service.Services
:return: None
"""
for escal in self:
# If no host, no hope of having a service
if not (hasattr(escal, 'host_name') and hasattr(escal, 'service_description')):
continue
es_hname, sdesc = escal.host_name, escal.service_description
if '' in (es_hname.strip(), sdesc.strip()):
continue
for hname in strip_and_uniq(es_hname.split(',')):
if sdesc.strip() == '*':
slist = services.find_srvs_by_hostname(hname)
if slist is not None:
for serv in slist:
serv.escalations.append(escal)
else:
for sname in strip_and_uniq(sdesc.split(',')):
serv = services.find_srv_by_name_and_hostname(hname, sname)
if serv is not None:
# print "Linking service", s.get_name(), 'with me', es.get_name()
serv.escalations.append(escal)
# print "Now service", s.get_name(), 'have', s.escalations
[docs] def linkify_es_by_h(self, hosts):
"""Add each escalation object into host.escalation attribute
:param hosts: host list, used to look for a specific host
:type hosts: alignak.objects.host.Hosts
:return: None
"""
for escal in self:
# If no host, no hope of having a service
if (not hasattr(escal, 'host_name') or escal.host_name.strip() == ''
or (hasattr(escal, 'service_description')
and escal.service_description.strip() != '')):
continue
# I must be NOT a escalation on for service
for hname in strip_and_uniq(escal.host_name.split(',')):
host = hosts.find_by_name(hname)
if host is not None:
# print "Linking host", h.get_name(), 'with me', es.get_name()
host.escalations.append(escal)
# print "Now host", h.get_name(), 'have', h.escalations
[docs] def explode(self, hosts, hostgroups, contactgroups):
"""Loop over all escalation and explode hostsgroups in host
and contactgroups in contacts
Call Item.explode_host_groups_into_hosts and Item.explode_contact_groups_into_contacts
:param hosts: host list to explode
:type hosts: alignak.objects.host.Hosts
:param hostgroups: hostgroup list to explode
:type hostgroups: alignak.objects.hostgroup.Hostgroups
:param contactgroups: contactgroup list to explode
:type contactgroups: alignak.objects.contactgroup.Contactgroups
:return: None
"""
for i in self:
# items::explode_host_groups_into_hosts
# take all hosts from our hostgroup_name into our host_name property
self.explode_host_groups_into_hosts(i, hosts, hostgroups)
# items::explode_contact_groups_into_contacts
# take all contacts from our contact_groups into our contact property
self.explode_contact_groups_into_contacts(i, contactgroups)