Use stevedore to load alarm rules api

Alarm rule evaluators are loadable with stevedore, but not the API side,
since wsme 0.6 it's now possible to do it.

This change adds that to allow to create a gnocchi alarm rule api and
evaluator in the gnocchi code tree.

This also move out the V2Controller API from __init__ to avoid
recursive import with stevedore.

Change-Id: I468f9f82b41eb7f4896dd93a8c62ce63b49535bb
This commit is contained in:
Mehdi Abaakouk 2015-02-05 09:49:07 +01:00
parent b4093e9347
commit 2e75325060
6 changed files with 131 additions and 85 deletions

View File

@ -15,7 +15,7 @@
import pecan
from ceilometer.api.controllers import v2
from ceilometer.api.controllers.v2 import root as v2
MEDIA_TYPE_JSON = 'application/vnd.openstack.telemetry-%s+json'
MEDIA_TYPE_XML = 'application/vnd.openstack.telemetry-%s+xml'

View File

@ -1,40 +0,0 @@
#
# Copyright 2012 New Dream Network, LLC (DreamHost)
# Copyright 2013 IBM Corp.
# Copyright 2013 eNovance <licensing@enovance.com>
# Copyright Ericsson AB 2013. All rights reserved
# Copyright 2014 Hewlett-Packard Company
# Copyright 2015 Huawei Technologies Co., Ltd.
#
# 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 ceilometer.api.controllers.v2 import alarms
from ceilometer.api.controllers.v2 import capabilities
from ceilometer.api.controllers.v2 import events
from ceilometer.api.controllers.v2 import meters
from ceilometer.api.controllers.v2 import query
from ceilometer.api.controllers.v2 import resources
from ceilometer.api.controllers.v2 import samples
class V2Controller(object):
"""Version 2 API controller root."""
resources = resources.ResourcesController()
meters = meters.MetersController()
samples = samples.SamplesController()
alarms = alarms.AlarmsController()
event_types = events.EventTypesController()
events = events.EventsController()
query = query.QueryController()
capabilities = capabilities.CapabilitiesController()

View File

@ -31,6 +31,7 @@ import pecan
from pecan import rest
import pytz
import six
from stevedore import extension
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
@ -133,7 +134,12 @@ class CronType(wtypes.UserType):
return value
class AlarmThresholdRule(base.Base):
class AlarmThresholdRule(base.AlarmRule):
"""Alarm Threshold Rule
Describe when to trigger the alarm based on computed statistics
"""
meter_name = wsme.wsattr(wtypes.text, mandatory=True)
"The name of the meter"
@ -186,6 +192,16 @@ class AlarmThresholdRule(base.Base):
allow_timestamps=False)
return threshold_rule
@staticmethod
def validate_alarm(alarm):
# ensure an implicit constraint on project_id is added to
# the query if not already present
alarm.threshold_rule.query = v2_utils.sanitize_query(
alarm.threshold_rule.query,
storage.SampleFilter.__init__,
on_behalf_of=alarm.project_id
)
@property
def default_description(self):
return (_('Alarm when %(meter_name)s is %(comparison_operator)s a '
@ -218,7 +234,13 @@ class AlarmThresholdRule(base.Base):
'type': 'string'}])
class AlarmCombinationRule(base.Base):
class AlarmCombinationRule(base.AlarmRule):
"""Alarm Combinarion Rule
Describe when to trigger the alarm based on combining the state of
other alarms.
"""
operator = base.AdvEnum('operator', str, 'or', 'and', default='and')
"How to combine the sub-alarms"
@ -242,6 +264,25 @@ class AlarmCombinationRule(base.Base):
'alarm ids.'))
return rule
@staticmethod
def validate_alarm(alarm):
project = v2_utils.get_auth_project(
alarm.project_id if alarm.project_id != wtypes.Unset else None)
for id in alarm.combination_rule.alarm_ids:
alarms = list(pecan.request.alarm_storage_conn.get_alarms(
alarm_id=id, project=project))
if not alarms:
raise AlarmNotFound(id, project)
@staticmethod
def update_hook(alarm):
# should check if there is any circle in the dependency, but for
# efficiency reason, here only check alarm cannot depend on itself
if alarm.alarm_id in alarm.combination_rule.alarm_ids:
raise base.ClientSideError(
_('Cannot specify alarm %s itself in combination rule') %
alarm.alarm_id)
@classmethod
def sample(cls):
return cls(operator='or',
@ -302,6 +343,10 @@ class AlarmTimeConstraint(base.Base):
timezone='Europe/Ljubljana')
ALARMS_RULES = extension.ExtensionManager("ceilometer.alarm.rule")
LOG.debug("alarm rules plugin loaded: %s" % ",".join(ALARMS_RULES.names()))
class Alarm(base.Base):
"""Representation of an alarm.
@ -347,17 +392,10 @@ class Alarm(base.Base):
repeat_actions = wsme.wsattr(bool, default=False)
"The actions should be re-triggered on each evaluation cycle"
type = base.AdvEnum('type', str, 'threshold', 'combination',
type = base.AdvEnum('type', str, *ALARMS_RULES.names(),
mandatory=True)
"Explicit type specifier to select which rule to follow below."
threshold_rule = AlarmThresholdRule
"Describe when to trigger the alarm based on computed statistics"
combination_rule = AlarmCombinationRule
"""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"""
@ -387,10 +425,9 @@ class Alarm(base.Base):
super(Alarm, self).__init__(**kwargs)
if rule:
if self.type == 'threshold':
self.threshold_rule = AlarmThresholdRule(**rule)
elif self.type == 'combination':
self.combination_rule = AlarmCombinationRule(**rule)
setattr(self, '%s_rule' % self.type,
ALARMS_RULES[self.type].plugin(**rule))
if time_constraints:
self.time_constraints = [AlarmTimeConstraint(**tc)
for tc in time_constraints]
@ -400,22 +437,8 @@ class Alarm(base.Base):
Alarm.check_rule(alarm)
Alarm.check_alarm_actions(alarm)
if alarm.threshold_rule:
# ensure an implicit constraint on project_id is added to
# the query if not already present
alarm.threshold_rule.query = v2_utils.sanitize_query(
alarm.threshold_rule.query,
storage.SampleFilter.__init__,
on_behalf_of=alarm.project_id
)
elif alarm.combination_rule:
project = v2_utils.get_auth_project(
alarm.project_id if alarm.project_id != wtypes.Unset else None)
for id in alarm.combination_rule.alarm_ids:
alarms = list(pecan.request.alarm_storage_conn.get_alarms(
alarm_id=id, project=project))
if not alarms:
raise AlarmNotFound(id, project)
ALARMS_RULES[alarm.type].plugin.validate_alarm(alarm)
tc_names = [tc.name for tc in alarm.time_constraints]
if len(tc_names) > len(set(tc_names)):
@ -432,9 +455,16 @@ class Alarm(base.Base):
error = _("%(rule)s must be set for %(type)s"
" type alarm") % {"rule": rule, "type": alarm.type}
raise base.ClientSideError(error)
if alarm.threshold_rule and alarm.combination_rule:
error = _("threshold_rule and combination_rule "
"cannot be set at the same time")
rule_set = None
for ext in ALARMS_RULES:
name = "%s_rule" % ext.name
if getattr(alarm, name):
if rule_set is None:
rule_set = name
else:
error = _("%(rule1)s and %(rule2)s cannot be set at the "
"same time") % {'rule1': rule_set, 'rule2': name}
raise base.ClientSideError(error)
@staticmethod
@ -462,8 +492,6 @@ class Alarm(base.Base):
name="SwiftObjectAlarm",
description="An alarm",
type='combination',
threshold_rule=None,
combination_rule=AlarmCombinationRule.sample(),
time_constraints=[AlarmTimeConstraint.sample().as_dict()],
user_id="c96c887c216949acbdfbd8b494863567",
project_id="c96c887c216949acbdfbd8b494863567",
@ -487,6 +515,9 @@ class Alarm(base.Base):
d['time_constraints'] = [tc.as_dict() for tc in self.time_constraints]
return d
Alarm.add_attributes(**{"%s_rule" % ext.name: ext.plugin
for ext in ALARMS_RULES})
class AlarmChange(base.Base):
"""Representation of an event in an alarm's history."""
@ -639,13 +670,7 @@ class AlarmController(rest.RestController):
_("Alarm with name=%s exists") % data.name,
status_code=409)
# should check if there is any circle in the dependency, but for
# efficiency reason, here only check alarm cannot depend on itself
if data.type == 'combination':
if self._id in data.combination_rule.alarm_ids:
raise base.ClientSideError(
_('Cannot specify alarm %s itself in '
'combination rule') % self._id)
ALARMS_RULES[data.type].plugin.update_hook(data)
old_alarm = Alarm.from_db_model(alarm_in).as_dict(alarm_models.Alarm)
updated_alarm = data.as_dict(alarm_models.Alarm)
@ -805,6 +830,8 @@ class AlarmsController(rest.RestController):
data.timestamp = now
data.state_timestamp = now
ALARMS_RULES[data.type].plugin.create_hook(data)
change = data.as_dict(alarm_models.Alarm)
# make sure alarms are unique by name per project.

View File

@ -83,7 +83,7 @@ class AdvEnum(wtypes.wsproperty):
value, e)
class Base(wtypes.Base):
class Base(wtypes.DynamicBase):
@classmethod
def from_db_model(cls, m):
@ -228,3 +228,18 @@ class Query(Base):
{'value': self.value, 'type': type})
raise ClientSideError(msg)
return converted_value
class AlarmRule(Base):
"""Base class Alarm Rule extension and wsme.types."""
@staticmethod
def validate_alarm(alarm):
pass
@staticmethod
def create_hook(alarm):
pass
@staticmethod
def update_hook(alarm):
pass

View File

@ -0,0 +1,40 @@
#
# Copyright 2012 New Dream Network, LLC (DreamHost)
# Copyright 2013 IBM Corp.
# Copyright 2013 eNovance <licensing@enovance.com>
# Copyright Ericsson AB 2013. All rights reserved
# Copyright 2014 Hewlett-Packard Company
# Copyright 2015 Huawei Technologies Co., Ltd.
#
# 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 ceilometer.api.controllers.v2 import alarms
from ceilometer.api.controllers.v2 import capabilities
from ceilometer.api.controllers.v2 import events
from ceilometer.api.controllers.v2 import meters
from ceilometer.api.controllers.v2 import query
from ceilometer.api.controllers.v2 import resources
from ceilometer.api.controllers.v2 import samples
class V2Controller(object):
"""Version 2 API controller root."""
resources = resources.ResourcesController()
meters = meters.MetersController()
samples = samples.SamplesController()
alarms = alarms.AlarmsController()
event_types = events.EventTypesController()
events = events.EventsController()
query = query.QueryController()
capabilities = capabilities.CapabilitiesController()

View File

@ -271,6 +271,10 @@ ceilometer.event.publisher =
direct = ceilometer.publisher.direct:DirectPublisher
notifier = ceilometer.publisher.messaging:EventNotifierPublisher
ceilometer.alarm.rule =
threshold = ceilometer.api.controllers.v2.alarms:AlarmThresholdRule
combination = ceilometer.api.controllers.v2.alarms:AlarmCombinationRule
ceilometer.alarm.evaluator =
threshold = ceilometer.alarm.evaluator.threshold:ThresholdEvaluator
combination = ceilometer.alarm.evaluator.combination:CombinationEvaluator