
This patch allows alarms to have time constraints specified. If one or more time constraints are specified, the alarm is evaluated only if the current time is within at least one time constraint. A field 'time_constraints' is added to the alarm model that holds a list of the time constraints. Each time constraint has the fields - name the name of the constraint - description description, default is auto-generated from start and duration - start starting point(s) of the constraint, in cron format - duration duration of the constraint, in seconds - timezone optional timezone information Change-Id: I2d1bcd6728affc31834d7e2f3a0bdd570b2413bb Blueprint: time-constrained-alarms
124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
# -*- encoding: utf-8 -*-
|
|
#
|
|
# Copyright © 2013 eNovance <licensing@enovance.com>
|
|
#
|
|
# Authors: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
|
|
import abc
|
|
import croniter
|
|
import datetime
|
|
import pytz
|
|
|
|
from ceilometerclient import client as ceiloclient
|
|
from oslo.config import cfg
|
|
import six
|
|
|
|
from ceilometer.openstack.common.gettextutils import _ # noqa
|
|
from ceilometer.openstack.common import log
|
|
from ceilometer.openstack.common import timeutils
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
UNKNOWN = 'insufficient data'
|
|
OK = 'ok'
|
|
ALARM = 'alarm'
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class Evaluator(object):
|
|
"""Base class for alarm rule evaluator plugins."""
|
|
|
|
def __init__(self, notifier):
|
|
self.notifier = notifier
|
|
self.api_client = None
|
|
|
|
@property
|
|
def _client(self):
|
|
"""Construct or reuse an authenticated API client."""
|
|
if not self.api_client:
|
|
auth_config = cfg.CONF.service_credentials
|
|
creds = dict(
|
|
os_auth_url=auth_config.os_auth_url,
|
|
os_region_name=auth_config.os_region_name,
|
|
os_tenant_name=auth_config.os_tenant_name,
|
|
os_password=auth_config.os_password,
|
|
os_username=auth_config.os_username,
|
|
os_cacert=auth_config.os_cacert,
|
|
os_endpoint_type=auth_config.os_endpoint_type,
|
|
)
|
|
self.api_client = ceiloclient.get_client(2, **creds)
|
|
return self.api_client
|
|
|
|
def _refresh(self, alarm, state, reason, reason_data):
|
|
"""Refresh alarm state."""
|
|
try:
|
|
previous = alarm.state
|
|
if previous != state:
|
|
LOG.info(_('alarm %(id)s transitioning to %(state)s because '
|
|
'%(reason)s') % {'id': alarm.alarm_id,
|
|
'state': state,
|
|
'reason': reason})
|
|
|
|
self._client.alarms.set_state(alarm.alarm_id, state=state)
|
|
alarm.state = state
|
|
if self.notifier:
|
|
self.notifier.notify(alarm, previous, reason, reason_data)
|
|
except Exception:
|
|
# retry will occur naturally on the next evaluation
|
|
# cycle (unless alarm state reverts in the meantime)
|
|
LOG.exception(_('alarm state update failed'))
|
|
|
|
@classmethod
|
|
def within_time_constraint(cls, alarm):
|
|
"""Check whether the alarm is within at least one of its time
|
|
constraints. If there are none, then the answer is yes.
|
|
"""
|
|
if not alarm.time_constraints:
|
|
return True
|
|
|
|
now_utc = timeutils.utcnow()
|
|
for tc in alarm.time_constraints:
|
|
tz = pytz.timezone(tc['timezone']) if tc['timezone'] else None
|
|
now_tz = now_utc.astimezone(tz) if tz else now_utc
|
|
start_cron = croniter.croniter(tc['start'], now_tz)
|
|
if cls._is_exact_match(start_cron, now_tz):
|
|
return True
|
|
latest_start = start_cron.get_prev(datetime.datetime)
|
|
duration = datetime.timedelta(seconds=tc['duration'])
|
|
if latest_start <= now_tz <= latest_start + duration:
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def _is_exact_match(cron, ts):
|
|
"""Handle edge case where if the timestamp is the same as the
|
|
cron point in time to the minute, croniter returns the previous
|
|
start, not the current. We can check this by first going one
|
|
step back and then one step forward and check if we are
|
|
at the original point in time.
|
|
"""
|
|
cron.get_prev()
|
|
diff = timeutils.total_seconds(ts - cron.get_next(datetime.datetime))
|
|
return abs(diff) < 60 # minute precision
|
|
|
|
@abc.abstractmethod
|
|
def evaluate(self, alarm):
|
|
'''interface definition
|
|
|
|
evaluate an alarm
|
|
alarm Alarm: an instance of the Alarm
|
|
'''
|