Merge "Adds time constraints to alarms"
This commit is contained in:
commit
3d10fb7aa4
@ -18,6 +18,9 @@
|
||||
|
||||
|
||||
import abc
|
||||
import croniter
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
from ceilometerclient import client as ceiloclient
|
||||
from oslo.config import cfg
|
||||
@ -25,6 +28,7 @@ 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__)
|
||||
|
||||
@ -77,6 +81,39 @@ class Evaluator(object):
|
||||
# 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
|
||||
|
@ -96,6 +96,11 @@ class CombinationEvaluator(evaluator.Evaluator):
|
||||
self._refresh(alarm, state, reason, reason_data)
|
||||
|
||||
def evaluate(self, alarm):
|
||||
if not self.within_time_constraint(alarm):
|
||||
LOG.debug(_('Attempted to evaluate alarm %s, but it is not '
|
||||
'within its time constraint.') % alarm.alarm_id)
|
||||
return
|
||||
|
||||
states = zip(alarm.rule['alarm_ids'],
|
||||
itertools.imap(self._get_alarm_state,
|
||||
alarm.rule['alarm_ids']))
|
||||
|
@ -173,6 +173,11 @@ class ThresholdEvaluator(evaluator.Evaluator):
|
||||
self._refresh(alarm, state, reason, reason_data)
|
||||
|
||||
def evaluate(self, alarm):
|
||||
if not self.within_time_constraint(alarm):
|
||||
LOG.debug(_('Attempted to evaluate alarm %s, but it is not '
|
||||
'within its time constraint.') % alarm.alarm_id)
|
||||
return
|
||||
|
||||
query = self._bound_duration(
|
||||
alarm,
|
||||
alarm.rule['query']
|
||||
|
@ -28,11 +28,13 @@
|
||||
import ast
|
||||
import base64
|
||||
import copy
|
||||
import croniter
|
||||
import datetime
|
||||
import functools
|
||||
import inspect
|
||||
import json
|
||||
import jsonschema
|
||||
import pytz
|
||||
import uuid
|
||||
|
||||
from oslo.config import cfg
|
||||
@ -108,6 +110,18 @@ class AdvEnum(wtypes.wsproperty):
|
||||
setattr(parent, self._name, value)
|
||||
|
||||
|
||||
class CronType(wtypes.UserType):
|
||||
"""A user type that represents a cron format."""
|
||||
basetype = six.string_types
|
||||
name = 'cron'
|
||||
|
||||
@staticmethod
|
||||
def validate(value):
|
||||
# raises ValueError if invalid
|
||||
croniter.croniter(value)
|
||||
return value
|
||||
|
||||
|
||||
class _Base(wtypes.Base):
|
||||
|
||||
@classmethod
|
||||
@ -1505,6 +1519,59 @@ class AlarmCombinationRule(_Base):
|
||||
'153462d0-a9b8-4b5b-8175-9e4b05e9b856'])
|
||||
|
||||
|
||||
class AlarmTimeConstraint(_Base):
|
||||
"""Representation of a time constraint on an alarm."""
|
||||
|
||||
name = wsme.wsattr(wtypes.text, mandatory=True)
|
||||
"The name of the constraint"
|
||||
|
||||
_description = None # provide a default
|
||||
|
||||
def get_description(self):
|
||||
if not self._description:
|
||||
return 'Time constraint at %s lasting for %s seconds' \
|
||||
% (self.start, self.duration)
|
||||
return self._description
|
||||
|
||||
def set_description(self, value):
|
||||
self._description = value
|
||||
|
||||
description = wsme.wsproperty(wtypes.text, get_description,
|
||||
set_description)
|
||||
"The description of the constraint"
|
||||
|
||||
start = wsme.wsattr(CronType(), mandatory=True)
|
||||
"Start point of the time constraint, in cron format"
|
||||
|
||||
duration = wsme.wsattr(wtypes.IntegerType(minimum=0), mandatory=True)
|
||||
"How long the constraint should last, in seconds"
|
||||
|
||||
timezone = wsme.wsattr(wtypes.text, default="")
|
||||
"Timezone of the constraint"
|
||||
|
||||
def as_dict(self):
|
||||
return self.as_dict_from_keys(['name', 'description', 'start',
|
||||
'duration', 'timezone'])
|
||||
|
||||
@staticmethod
|
||||
def validate(tc):
|
||||
if tc.timezone:
|
||||
try:
|
||||
pytz.timezone(tc.timezone)
|
||||
except Exception:
|
||||
raise ClientSideError(_("Timezone %s is not valid")
|
||||
% tc.timezone)
|
||||
return tc
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(name='SampleConstraint',
|
||||
description='nightly build every night at 23h for 3 hours',
|
||||
start='0 23 * * *',
|
||||
duration=10800,
|
||||
timezone='Europe/Ljubljana')
|
||||
|
||||
|
||||
class Alarm(_Base):
|
||||
"""Representation of an alarm.
|
||||
|
||||
@ -1560,6 +1627,9 @@ class Alarm(_Base):
|
||||
"""Describe when to trigger the alarm based on combining the state of
|
||||
other alarms"""
|
||||
|
||||
time_constraints = wtypes.wsattr([AlarmTimeConstraint], default=[])
|
||||
"""Describe time constraints for the alarm"""
|
||||
|
||||
# These settings are ignored in the PUT or POST operations, but are
|
||||
# filled in for GET
|
||||
project_id = wtypes.text
|
||||
@ -1578,7 +1648,7 @@ class Alarm(_Base):
|
||||
state_timestamp = datetime.datetime
|
||||
"The date of the last alarm state changed"
|
||||
|
||||
def __init__(self, rule=None, **kwargs):
|
||||
def __init__(self, rule=None, time_constraints=None, **kwargs):
|
||||
super(Alarm, self).__init__(**kwargs)
|
||||
|
||||
if rule:
|
||||
@ -1586,6 +1656,9 @@ class Alarm(_Base):
|
||||
self.threshold_rule = AlarmThresholdRule(**rule)
|
||||
elif self.type == 'combination':
|
||||
self.combination_rule = AlarmCombinationRule(**rule)
|
||||
if time_constraints:
|
||||
self.time_constraints = [AlarmTimeConstraint(**tc)
|
||||
for tc in time_constraints]
|
||||
|
||||
@staticmethod
|
||||
def validate(alarm):
|
||||
@ -1628,6 +1701,7 @@ class Alarm(_Base):
|
||||
type='combination',
|
||||
threshold_rule=None,
|
||||
combination_rule=AlarmCombinationRule.sample(),
|
||||
time_constraints=[AlarmTimeConstraint.sample().as_dict()],
|
||||
user_id="c96c887c216949acbdfbd8b494863567",
|
||||
project_id="c96c887c216949acbdfbd8b494863567",
|
||||
enabled=True,
|
||||
@ -1646,6 +1720,7 @@ class Alarm(_Base):
|
||||
if k.endswith('_rule'):
|
||||
del d[k]
|
||||
d['rule'] = getattr(self, "%s_rule" % self.type).as_dict()
|
||||
d['time_constraints'] = [tc.as_dict() for tc in self.time_constraints]
|
||||
return d
|
||||
|
||||
|
||||
|
@ -785,6 +785,7 @@ class Connection(base.Connection):
|
||||
insufficient_data_actions=
|
||||
row.insufficient_data_actions,
|
||||
rule=row.rule,
|
||||
time_constraints=row.time_constraints,
|
||||
repeat_actions=row.repeat_actions)
|
||||
|
||||
def _retrieve_alarms(self, query):
|
||||
|
@ -299,6 +299,7 @@ class Alarm(Model):
|
||||
:param project_id: the project_id of the creator
|
||||
:param evaluation_periods: the number of periods
|
||||
:param period: the time period in seconds
|
||||
:param time_constraints: the list of the alarm's time constraints, if any
|
||||
:param timestamp: the timestamp when the alarm was last updated
|
||||
:param state_timestamp: the timestamp of the last state change
|
||||
:param ok_actions: the list of webhooks to call when entering the ok state
|
||||
@ -312,7 +313,7 @@ class Alarm(Model):
|
||||
def __init__(self, alarm_id, type, enabled, name, description,
|
||||
timestamp, user_id, project_id, state, state_timestamp,
|
||||
ok_actions, alarm_actions, insufficient_data_actions,
|
||||
repeat_actions, rule):
|
||||
repeat_actions, rule, time_constraints):
|
||||
Model.__init__(
|
||||
self,
|
||||
alarm_id=alarm_id,
|
||||
@ -330,7 +331,8 @@ class Alarm(Model):
|
||||
insufficient_data_actions=
|
||||
insufficient_data_actions,
|
||||
repeat_actions=repeat_actions,
|
||||
rule=rule)
|
||||
rule=rule,
|
||||
time_constraints=time_constraints)
|
||||
|
||||
|
||||
class AlarmChange(Model):
|
||||
|
@ -209,6 +209,7 @@ class Connection(base.Connection):
|
||||
stored_alarm = self.db.alarm.find({'alarm_id': alarm.alarm_id})[0]
|
||||
del stored_alarm['_id']
|
||||
self._ensure_encapsulated_rule_format(stored_alarm)
|
||||
self._ensure_time_constraints(stored_alarm)
|
||||
return models.Alarm(**stored_alarm)
|
||||
|
||||
create_alarm = update_alarm
|
||||
@ -317,6 +318,7 @@ class Connection(base.Connection):
|
||||
a.update(alarm)
|
||||
del a['_id']
|
||||
self._ensure_encapsulated_rule_format(a)
|
||||
self._ensure_time_constraints(a)
|
||||
yield models.Alarm(**a)
|
||||
|
||||
def _retrieve_alarm_changes(self, query_filter, orderby, limit):
|
||||
@ -397,6 +399,12 @@ class Connection(base.Connection):
|
||||
new_matching_metadata[elem['key']] = elem['value']
|
||||
return new_matching_metadata
|
||||
|
||||
@staticmethod
|
||||
def _ensure_time_constraints(alarm):
|
||||
"""Ensures the alarm has a time constraints field."""
|
||||
if 'time_constraints' not in alarm:
|
||||
alarm['time_constraints'] = []
|
||||
|
||||
|
||||
class QueryTransformer(object):
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Author: Nejc Saje <nejc.saje@xlab.si>
|
||||
#
|
||||
# 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.
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import Text
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData(bind=migrate_engine)
|
||||
alarm = Table('alarm', meta, autoload=True)
|
||||
time_constraints = Column('time_constraints', Text())
|
||||
alarm.create_column(time_constraints)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData(bind=migrate_engine)
|
||||
alarm = Table('alarm', meta, autoload=True)
|
||||
time_constraints = Column('time_constraints', Text())
|
||||
alarm.drop_column(time_constraints)
|
@ -309,6 +309,7 @@ class Alarm(Base):
|
||||
repeat_actions = Column(Boolean)
|
||||
|
||||
rule = Column(JSONEncodedDict)
|
||||
time_constraints = Column(JSONEncodedDict)
|
||||
|
||||
|
||||
class AlarmChange(Base):
|
||||
|
@ -17,10 +17,13 @@
|
||||
# under the License.
|
||||
"""class for tests in ceilometer/alarm/evaluator/__init__.py
|
||||
"""
|
||||
import datetime
|
||||
import mock
|
||||
import pytz
|
||||
|
||||
from ceilometer.alarm import evaluator
|
||||
from ceilometer.openstack.common import test
|
||||
from ceilometer.openstack.common import timeutils
|
||||
|
||||
|
||||
class TestEvaluatorBaseClass(test.BaseTestCase):
|
||||
@ -45,3 +48,92 @@ class TestEvaluatorBaseClass(test.BaseTestCase):
|
||||
ev._refresh(mock.MagicMock(), mock.MagicMock(),
|
||||
mock.MagicMock(), mock.MagicMock())
|
||||
self.assertTrue(self.called)
|
||||
|
||||
def test_base_time_constraints(self):
|
||||
alarm = mock.MagicMock()
|
||||
alarm.time_constraints = [
|
||||
{'name': 'test',
|
||||
'description': 'test',
|
||||
'start': '0 11 * * *', # daily at 11:00
|
||||
'duration': 10800, # 3 hours
|
||||
'timezone': ''},
|
||||
{'name': 'test2',
|
||||
'description': 'test',
|
||||
'start': '0 23 * * *', # daily at 23:00
|
||||
'duration': 10800, # 3 hours
|
||||
'timezone': ''},
|
||||
]
|
||||
cls = evaluator.Evaluator
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 1, 12, 0, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 2, 1, 0, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 2, 5, 0, 0))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
|
||||
def test_base_time_constraints_complex(self):
|
||||
alarm = mock.MagicMock()
|
||||
alarm.time_constraints = [
|
||||
{'name': 'test',
|
||||
'description': 'test',
|
||||
# Every consecutive 2 minutes (from the 3rd to the 57th) past
|
||||
# every consecutive 2 hours (between 3:00 and 12:59) on every day.
|
||||
'start': '3-57/2 3-12/2 * * *',
|
||||
'duration': 30,
|
||||
'timezone': ''}
|
||||
]
|
||||
cls = evaluator.Evaluator
|
||||
|
||||
# test minutes inside
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 3, 3, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 3, 31, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 3, 57, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
|
||||
# test minutes outside
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 3, 2, 0))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 3, 4, 0))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 3, 58, 0))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
|
||||
# test hours inside
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 3, 31, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 5, 31, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 11, 31, 0))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
|
||||
# test hours outside
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 1, 31, 0))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 4, 31, 0))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
timeutils.set_time_override(datetime.datetime(2014, 1, 5, 12, 31, 0))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
|
||||
def test_base_time_constraints_timezone(self):
|
||||
alarm = mock.MagicMock()
|
||||
alarm.time_constraints = [
|
||||
{'name': 'test',
|
||||
'description': 'test',
|
||||
'start': '0 11 * * *', # daily at 11:00
|
||||
'duration': 10800, # 3 hours
|
||||
'timezone': 'Europe/Ljubljana'}
|
||||
]
|
||||
cls = evaluator.Evaluator
|
||||
dt_eu = datetime.datetime(2014, 1, 1, 12, 0, 0,
|
||||
tzinfo=pytz.timezone('Europe/Ljubljana'))
|
||||
dt_us = datetime.datetime(2014, 1, 1, 12, 0, 0,
|
||||
tzinfo=pytz.timezone('US/Eastern'))
|
||||
timeutils.set_time_override(dt_eu.astimezone(pytz.UTC))
|
||||
self.assertTrue(cls.within_time_constraint(alarm))
|
||||
|
||||
timeutils.set_time_override(dt_us.astimezone(pytz.UTC))
|
||||
self.assertFalse(cls.within_time_constraint(alarm))
|
||||
|
@ -17,10 +17,13 @@
|
||||
# under the License.
|
||||
"""Tests for ceilometer/alarm/threshold_evaluation.py
|
||||
"""
|
||||
import datetime
|
||||
import mock
|
||||
import pytz
|
||||
import uuid
|
||||
|
||||
from ceilometer.alarm.evaluator import combination
|
||||
from ceilometer.openstack.common import timeutils
|
||||
from ceilometer.storage import models
|
||||
from ceilometer.tests.alarm.evaluator import base
|
||||
from ceilometerclient import exc
|
||||
@ -46,6 +49,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
ok_actions=[],
|
||||
alarm_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[],
|
||||
rule=dict(
|
||||
alarm_ids=[
|
||||
'9cfc3e51-2ff1-4b1d-ac01-c1bd4c6d0d1e',
|
||||
@ -66,6 +70,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
ok_actions=[],
|
||||
alarm_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[],
|
||||
rule=dict(
|
||||
alarm_ids=[
|
||||
'b82734f4-9d06-48f3-8a86-fa59a0c99dc8',
|
||||
@ -304,3 +309,69 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
in zip(self.alarms, reasons, reason_datas)]
|
||||
|
||||
self.assertEqual(self.notifier.notify.call_args_list, expected)
|
||||
|
||||
def test_state_change_inside_time_constraint(self):
|
||||
self._set_all_alarms('insufficient data')
|
||||
self.alarms[0].time_constraints = [
|
||||
{'name': 'test',
|
||||
'description': 'test',
|
||||
'start': '0 11 * * *', # daily at 11:00
|
||||
'duration': 10800, # 3 hours
|
||||
'timezone': 'Europe/Ljubljana'}
|
||||
]
|
||||
self.alarms[1].time_constraints = self.alarms[0].time_constraints
|
||||
dt = datetime.datetime(2014, 1, 1, 12, 0, 0,
|
||||
tzinfo=pytz.timezone('Europe/Ljubljana'))
|
||||
with mock.patch('ceilometerclient.client.get_client',
|
||||
return_value=self.api_client):
|
||||
timeutils.set_time_override(dt.astimezone(pytz.UTC))
|
||||
self.api_client.alarms.get.side_effect = [
|
||||
self._get_alarm('ok'),
|
||||
self._get_alarm('ok'),
|
||||
self._get_alarm('ok'),
|
||||
self._get_alarm('ok'),
|
||||
]
|
||||
self._evaluate_all_alarms()
|
||||
expected = [mock.call(alarm.alarm_id, state='ok')
|
||||
for alarm in self.alarms]
|
||||
update_calls = self.api_client.alarms.set_state.call_args_list
|
||||
self.assertEqual(expected, update_calls,
|
||||
"Alarm should change state if the current "
|
||||
"time is inside its time constraint.")
|
||||
reasons, reason_datas = self._combination_transition_reason(
|
||||
'ok',
|
||||
self.alarms[0].rule['alarm_ids'],
|
||||
self.alarms[1].rule['alarm_ids'])
|
||||
expected = [mock.call(alarm, 'insufficient data',
|
||||
reason, reason_data)
|
||||
for alarm, reason, reason_data
|
||||
in zip(self.alarms, reasons, reason_datas)]
|
||||
self.assertEqual(expected, self.notifier.notify.call_args_list)
|
||||
|
||||
def test_no_state_change_outside_time_constraint(self):
|
||||
self._set_all_alarms('insufficient data')
|
||||
self.alarms[0].time_constraints = [
|
||||
{'name': 'test',
|
||||
'description': 'test',
|
||||
'start': '0 11 * * *', # daily at 11:00
|
||||
'duration': 10800, # 3 hours
|
||||
'timezone': 'Europe/Ljubljana'}
|
||||
]
|
||||
self.alarms[1].time_constraints = self.alarms[0].time_constraints
|
||||
dt = datetime.datetime(2014, 1, 1, 15, 0, 0,
|
||||
tzinfo=pytz.timezone('Europe/Ljubljana'))
|
||||
with mock.patch('ceilometerclient.client.get_client',
|
||||
return_value=self.api_client):
|
||||
timeutils.set_time_override(dt.astimezone(pytz.UTC))
|
||||
self.api_client.alarms.get.side_effect = [
|
||||
self._get_alarm('ok'),
|
||||
self._get_alarm('ok'),
|
||||
self._get_alarm('ok'),
|
||||
self._get_alarm('ok'),
|
||||
]
|
||||
self._evaluate_all_alarms()
|
||||
update_calls = self.api_client.alarms.set_state.call_args_list
|
||||
self.assertEqual([], update_calls,
|
||||
"Alarm should not change state if the current "
|
||||
" time is outside its time constraint.")
|
||||
self.assertEqual([], self.notifier.notify.call_args_list)
|
||||
|
@ -19,6 +19,7 @@
|
||||
"""
|
||||
import datetime
|
||||
import mock
|
||||
import pytz
|
||||
import uuid
|
||||
|
||||
from six import moves
|
||||
@ -51,6 +52,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
ok_actions=[],
|
||||
alarm_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[],
|
||||
rule=dict(
|
||||
comparison_operator='gt',
|
||||
threshold=80.0,
|
||||
@ -79,6 +81,7 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
alarm_actions=[],
|
||||
repeat_actions=False,
|
||||
alarm_id=str(uuid.uuid4()),
|
||||
time_constraints=[],
|
||||
rule=dict(
|
||||
comparison_operator='le',
|
||||
threshold=10.0,
|
||||
@ -438,3 +441,64 @@ class TestEvaluate(base.TestEvaluatorBase):
|
||||
|
||||
def test_simple_alarm_no_clear_without_outlier_exclusion(self):
|
||||
self. _do_test_simple_alarm_clear_outlier_exclusion(False)
|
||||
|
||||
def test_state_change_inside_time_constraint(self):
|
||||
self._set_all_alarms('ok')
|
||||
self.alarms[0].time_constraints = [
|
||||
{'name': 'test',
|
||||
'description': 'test',
|
||||
'start': '0 11 * * *', # daily at 11:00
|
||||
'duration': 10800, # 3 hours
|
||||
'timezone': 'Europe/Ljubljana'}
|
||||
]
|
||||
self.alarms[1].time_constraints = self.alarms[0].time_constraints
|
||||
dt = datetime.datetime(2014, 1, 1, 12, 0, 0,
|
||||
tzinfo=pytz.timezone('Europe/Ljubljana'))
|
||||
with mock.patch('ceilometerclient.client.get_client',
|
||||
return_value=self.api_client):
|
||||
timeutils.set_time_override(dt.astimezone(pytz.UTC))
|
||||
# the following part based on test_simple_insufficient
|
||||
self.api_client.statistics.list.return_value = []
|
||||
self._evaluate_all_alarms()
|
||||
self._assert_all_alarms('insufficient data')
|
||||
expected = [mock.call(alarm.alarm_id,
|
||||
state='insufficient data')
|
||||
for alarm in self.alarms]
|
||||
update_calls = self.api_client.alarms.set_state.call_args_list
|
||||
self.assertEqual(expected, update_calls,
|
||||
"Alarm should change state if the current "
|
||||
"time is inside its time constraint.")
|
||||
expected = [mock.call(
|
||||
alarm,
|
||||
'ok',
|
||||
('%d datapoints are unknown'
|
||||
% alarm.rule['evaluation_periods']),
|
||||
self._reason_data('unknown',
|
||||
alarm.rule['evaluation_periods'],
|
||||
None))
|
||||
for alarm in self.alarms]
|
||||
self.assertEqual(expected, self.notifier.notify.call_args_list)
|
||||
|
||||
def test_no_state_change_outside_time_constraint(self):
|
||||
self._set_all_alarms('ok')
|
||||
self.alarms[0].time_constraints = [
|
||||
{'name': 'test',
|
||||
'description': 'test',
|
||||
'start': '0 11 * * *', # daily at 11:00
|
||||
'duration': 10800, # 3 hours
|
||||
'timezone': 'Europe/Ljubljana'}
|
||||
]
|
||||
self.alarms[1].time_constraints = self.alarms[0].time_constraints
|
||||
dt = datetime.datetime(2014, 1, 1, 15, 0, 0,
|
||||
tzinfo=pytz.timezone('Europe/Ljubljana'))
|
||||
with mock.patch('ceilometerclient.client.get_client',
|
||||
return_value=self.api_client):
|
||||
timeutils.set_time_override(dt.astimezone(pytz.UTC))
|
||||
self.api_client.statistics.list.return_value = []
|
||||
self._evaluate_all_alarms()
|
||||
self._assert_all_alarms('ok')
|
||||
update_calls = self.api_client.alarms.set_state.call_args_list
|
||||
self.assertEqual([], update_calls,
|
||||
"Alarm should not change state if the current "
|
||||
" time is outside its time constraint.")
|
||||
self.assertEqual([], self.notifier.notify.call_args_list)
|
||||
|
@ -100,6 +100,7 @@ class TestCoordinate(test.BaseTestCase):
|
||||
alarm_actions=[],
|
||||
insufficient_data_actions=[],
|
||||
alarm_id=uuid,
|
||||
time_constraints=[],
|
||||
rule=dict(
|
||||
statistic='avg',
|
||||
comparison_operator='gt',
|
||||
|
@ -70,6 +70,9 @@ class TestAlarms(FunctionalTest,
|
||||
repeat_actions=True,
|
||||
user_id=self.auth_headers['X-User-Id'],
|
||||
project_id=self.auth_headers['X-Project-Id'],
|
||||
time_constraints=[dict(name='testcons',
|
||||
start='0 11 * * *',
|
||||
duration=300)],
|
||||
rule=dict(comparison_operator='gt',
|
||||
threshold=2.0,
|
||||
statistic='avg',
|
||||
@ -95,6 +98,7 @@ class TestAlarms(FunctionalTest,
|
||||
repeat_actions=False,
|
||||
user_id=self.auth_headers['X-User-Id'],
|
||||
project_id=self.auth_headers['X-Project-Id'],
|
||||
time_constraints=[],
|
||||
rule=dict(comparison_operator='gt',
|
||||
threshold=4.0,
|
||||
statistic='avg',
|
||||
@ -120,6 +124,7 @@ class TestAlarms(FunctionalTest,
|
||||
repeat_actions=False,
|
||||
user_id=self.auth_headers['X-User-Id'],
|
||||
project_id=self.auth_headers['X-Project-Id'],
|
||||
time_constraints=[],
|
||||
rule=dict(comparison_operator='gt',
|
||||
threshold=3.0,
|
||||
statistic='avg',
|
||||
@ -145,6 +150,7 @@ class TestAlarms(FunctionalTest,
|
||||
repeat_actions=False,
|
||||
user_id=self.auth_headers['X-User-Id'],
|
||||
project_id=self.auth_headers['X-Project-Id'],
|
||||
time_constraints=[],
|
||||
rule=dict(alarm_ids=['a', 'b'],
|
||||
operator='or')
|
||||
)]:
|
||||
@ -215,6 +221,8 @@ class TestAlarms(FunctionalTest,
|
||||
'meter.test')
|
||||
self.assertEqual(one['alarm_id'], alarms[0]['alarm_id'])
|
||||
self.assertEqual(one['repeat_actions'], alarms[0]['repeat_actions'])
|
||||
self.assertEqual(one['time_constraints'],
|
||||
alarms[0]['time_constraints'])
|
||||
|
||||
def test_get_alarm_disabled(self):
|
||||
alarm = models.Alarm(name='disabled',
|
||||
@ -231,6 +239,7 @@ class TestAlarms(FunctionalTest,
|
||||
repeat_actions=False,
|
||||
user_id=self.auth_headers['X-User-Id'],
|
||||
project_id=self.auth_headers['X-Project-Id'],
|
||||
time_constraints=[],
|
||||
rule=dict(alarm_ids=['a', 'b'], operator='or'))
|
||||
self.conn.update_alarm(alarm)
|
||||
|
||||
@ -308,6 +317,70 @@ class TestAlarms(FunctionalTest,
|
||||
alarms = list(self.conn.get_alarms())
|
||||
self.assertEqual(4, len(alarms))
|
||||
|
||||
def test_post_invalid_alarm_time_constraint_start(self):
|
||||
json = {
|
||||
'name': 'added_alarm_invalid_constraint_duration',
|
||||
'type': 'threshold',
|
||||
'time_constraints': [
|
||||
{
|
||||
'name': 'testcons',
|
||||
'start': '11:00am',
|
||||
'duration': 10
|
||||
}
|
||||
],
|
||||
'threshold_rule': {
|
||||
'meter_name': 'ameter',
|
||||
'threshold': 300.0
|
||||
}
|
||||
}
|
||||
self.post_json('/alarms', params=json, expect_errors=True, status=400,
|
||||
headers=self.auth_headers)
|
||||
alarms = list(self.conn.get_alarms())
|
||||
self.assertEqual(4, len(alarms))
|
||||
|
||||
def test_post_invalid_alarm_time_constraint_duration(self):
|
||||
json = {
|
||||
'name': 'added_alarm_invalid_constraint_duration',
|
||||
'type': 'threshold',
|
||||
'time_constraints': [
|
||||
{
|
||||
'name': 'testcons',
|
||||
'start': '* 11 * * *',
|
||||
'duration': -1,
|
||||
}
|
||||
],
|
||||
'threshold_rule': {
|
||||
'meter_name': 'ameter',
|
||||
'threshold': 300.0
|
||||
}
|
||||
}
|
||||
self.post_json('/alarms', params=json, expect_errors=True, status=400,
|
||||
headers=self.auth_headers)
|
||||
alarms = list(self.conn.get_alarms())
|
||||
self.assertEqual(4, len(alarms))
|
||||
|
||||
def test_post_invalid_alarm_time_constraint_timezone(self):
|
||||
json = {
|
||||
'name': 'added_alarm_invalid_constraint_timezone',
|
||||
'type': 'threshold',
|
||||
'time_constraints': [
|
||||
{
|
||||
'name': 'testcons',
|
||||
'start': '* 11 * * *',
|
||||
'duration': 10,
|
||||
'timezone': 'aaaa'
|
||||
}
|
||||
],
|
||||
'threshold_rule': {
|
||||
'meter_name': 'ameter',
|
||||
'threshold': 300.0
|
||||
}
|
||||
}
|
||||
self.post_json('/alarms', params=json, expect_errors=True, status=400,
|
||||
headers=self.auth_headers)
|
||||
alarms = list(self.conn.get_alarms())
|
||||
self.assertEqual(4, len(alarms))
|
||||
|
||||
def test_post_invalid_alarm_period(self):
|
||||
json = {
|
||||
'name': 'added_alarm_invalid_period',
|
||||
@ -1120,8 +1193,9 @@ class TestAlarms(FunctionalTest,
|
||||
self.assertIsNotNone(actual['event_id'])
|
||||
|
||||
def _assert_in_json(self, expected, actual):
|
||||
actual = jsonutils.dumps(jsonutils.loads(actual), sort_keys=True)
|
||||
for k, v in expected.iteritems():
|
||||
fragment = jsonutils.dumps({k: v})[1:-1]
|
||||
fragment = jsonutils.dumps({k: v}, sort_keys=True)[1:-1]
|
||||
self.assertTrue(fragment in actual,
|
||||
'%s not in %s' % (fragment, actual))
|
||||
|
||||
|
@ -301,6 +301,7 @@ class TestQueryAlarmsController(tests_api.FunctionalTest,
|
||||
repeat_actions=True,
|
||||
user_id="user-id%d" % id,
|
||||
project_id=project_id,
|
||||
time_constraints=[],
|
||||
rule=dict(comparison_operator='gt',
|
||||
threshold=2.0,
|
||||
statistic='avg',
|
||||
|
@ -256,6 +256,10 @@ class CompatibilityTest(test_storage_scenarios.DBTestBase,
|
||||
self.assertEqual(old.rule['comparison_operator'], 'lt')
|
||||
self.assertEqual(old.rule['threshold'], 36)
|
||||
|
||||
def test_alarm_without_time_constraints(self):
|
||||
old = list(self.conn.get_alarms(name='other-old-alaert'))[0]
|
||||
self.assertEqual([], old.time_constraints)
|
||||
|
||||
def test_counter_unit(self):
|
||||
meters = list(self.conn.get_meters())
|
||||
self.assertEqual(len(meters), 1)
|
||||
|
@ -71,7 +71,8 @@ class ModelTest(test.BaseTestCase):
|
||||
alarm_fields = ["alarm_id", "type", "enabled", "name", "description",
|
||||
"timestamp", "user_id", "project_id", "state",
|
||||
"state_timestamp", "ok_actions", "alarm_actions",
|
||||
"insufficient_data_actions", "repeat_actions", "rule"]
|
||||
"insufficient_data_actions", "repeat_actions", "rule",
|
||||
"time_constraints"]
|
||||
|
||||
self.assertEqual(set(alarm_fields),
|
||||
set(models.Alarm.get_field_names()))
|
||||
|
@ -696,6 +696,7 @@ class RawSampleTest(DBTestBase,
|
||||
alarm_actions=['http://nowhere/alarms'],
|
||||
insufficient_data_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[],
|
||||
rule=dict(comparison_operator='eq',
|
||||
threshold=36,
|
||||
statistic='count',
|
||||
@ -2312,6 +2313,9 @@ class AlarmTestBase(DBTestBase):
|
||||
alarm_actions=['http://nowhere/alarms'],
|
||||
insufficient_data_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[dict(name='testcons',
|
||||
start='0 11 * * *',
|
||||
duration=300)],
|
||||
rule=dict(comparison_operator='eq',
|
||||
threshold=36,
|
||||
statistic='count',
|
||||
@ -2337,6 +2341,7 @@ class AlarmTestBase(DBTestBase):
|
||||
alarm_actions=['http://nowhere/alarms'],
|
||||
insufficient_data_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[],
|
||||
rule=dict(comparison_operator='gt',
|
||||
threshold=75,
|
||||
statistic='avg',
|
||||
@ -2362,6 +2367,7 @@ class AlarmTestBase(DBTestBase):
|
||||
alarm_actions=['http://nowhere/alarms'],
|
||||
insufficient_data_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[],
|
||||
rule=dict(comparison_operator='lt',
|
||||
threshold=10,
|
||||
statistic='min',
|
||||
@ -2446,6 +2452,7 @@ class AlarmTest(AlarmTestBase,
|
||||
alarm_actions=[],
|
||||
insufficient_data_actions=[],
|
||||
repeat_actions=False,
|
||||
time_constraints=[],
|
||||
rule=dict(comparison_operator='lt',
|
||||
threshold=34,
|
||||
statistic='max',
|
||||
|
@ -57,6 +57,9 @@ Alarms
|
||||
.. autotype:: ceilometer.api.controllers.v2.AlarmCombinationRule
|
||||
:members:
|
||||
|
||||
.. autotype:: ceilometer.api.controllers.v2.AlarmTimeConstraint
|
||||
:members:
|
||||
|
||||
.. autotype:: ceilometer.api.controllers.v2.AlarmChange
|
||||
:members:
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
alembic>=0.4.1
|
||||
anyjson>=0.3.3
|
||||
argparse
|
||||
croniter>=0.3.4
|
||||
eventlet>=0.13.0
|
||||
Flask>=0.10,<1.0
|
||||
happybase>=0.4,<=0.6
|
||||
@ -20,6 +21,7 @@ python-glanceclient>=0.9.0
|
||||
python-keystoneclient>=0.6.0
|
||||
python-novaclient>=2.15.0
|
||||
python-swiftclient>=1.6
|
||||
pytz>=2010h
|
||||
PyYAML>=3.1.0
|
||||
requests>=1.1
|
||||
six>=1.5.2
|
||||
|
Loading…
x
Reference in New Issue
Block a user