Merge "Adds time constraints to alarms"

This commit is contained in:
Jenkins 2014-03-04 18:37:16 +00:00 committed by Gerrit Code Review
commit 3d10fb7aa4
20 changed files with 493 additions and 5 deletions

View File

@ -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

View File

@ -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']))

View File

@ -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']

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -309,6 +309,7 @@ class Alarm(Base):
repeat_actions = Column(Boolean)
rule = Column(JSONEncodedDict)
time_constraints = Column(JSONEncodedDict)
class AlarmChange(Base):

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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',

View File

@ -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))

View File

@ -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',

View File

@ -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)

View File

@ -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()))

View File

@ -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',

View File

@ -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:

View File

@ -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