475 lines
17 KiB
Python
475 lines
17 KiB
Python
#
|
|
# 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 six
|
|
|
|
from heat.common import exception
|
|
from heat.common.i18n import _
|
|
from heat.engine import constraints
|
|
from heat.engine import properties
|
|
from heat.engine import resource
|
|
from heat.engine import support
|
|
from heat.engine import watchrule
|
|
|
|
|
|
COMMON_PROPERTIES = (
|
|
ALARM_ACTIONS, OK_ACTIONS, REPEAT_ACTIONS,
|
|
INSUFFICIENT_DATA_ACTIONS, DESCRIPTION, ENABLED, TIME_CONSTRAINTS,
|
|
SEVERITY,
|
|
) = (
|
|
'alarm_actions', 'ok_actions', 'repeat_actions',
|
|
'insufficient_data_actions', 'description', 'enabled', 'time_constraints',
|
|
'severity',
|
|
)
|
|
|
|
_TIME_CONSTRAINT_KEYS = (
|
|
NAME, START, DURATION, TIMEZONE, TIME_CONSTRAINT_DESCRIPTION,
|
|
) = (
|
|
'name', 'start', 'duration', 'timezone', 'description',
|
|
)
|
|
|
|
common_properties_schema = {
|
|
DESCRIPTION: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Description for the alarm.'),
|
|
update_allowed=True
|
|
),
|
|
ENABLED: properties.Schema(
|
|
properties.Schema.BOOLEAN,
|
|
_('True if alarm evaluation/actioning is enabled.'),
|
|
default='true',
|
|
update_allowed=True
|
|
),
|
|
ALARM_ACTIONS: properties.Schema(
|
|
properties.Schema.LIST,
|
|
_('A list of URLs (webhooks) to invoke when state transitions to '
|
|
'alarm.'),
|
|
update_allowed=True
|
|
),
|
|
OK_ACTIONS: properties.Schema(
|
|
properties.Schema.LIST,
|
|
_('A list of URLs (webhooks) to invoke when state transitions to '
|
|
'ok.'),
|
|
update_allowed=True
|
|
),
|
|
INSUFFICIENT_DATA_ACTIONS: properties.Schema(
|
|
properties.Schema.LIST,
|
|
_('A list of URLs (webhooks) to invoke when state transitions to '
|
|
'insufficient-data.'),
|
|
update_allowed=True
|
|
),
|
|
REPEAT_ACTIONS: properties.Schema(
|
|
properties.Schema.BOOLEAN,
|
|
_("False to trigger actions when the threshold is reached AND "
|
|
"the alarm's state has changed. By default, actions are called "
|
|
"each time the threshold is reached."),
|
|
default='true',
|
|
update_allowed=True
|
|
),
|
|
SEVERITY: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Severity of the alarm.'),
|
|
default='low',
|
|
constraints=[
|
|
constraints.AllowedValues(['low', 'moderate', 'critical'])
|
|
],
|
|
update_allowed=True,
|
|
support_status=support.SupportStatus(version='5.0.0'),
|
|
),
|
|
TIME_CONSTRAINTS: properties.Schema(
|
|
properties.Schema.LIST,
|
|
_('Describe time constraints for the alarm. '
|
|
'Only evaluate the alarm if the time at evaluation '
|
|
'is within this time constraint. Start point(s) of '
|
|
'the constraint are specified with a cron expression, '
|
|
'whereas its duration is given in seconds.'
|
|
),
|
|
schema=properties.Schema(
|
|
properties.Schema.MAP,
|
|
schema={
|
|
NAME: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_("Name for the time constraint."),
|
|
required=True
|
|
),
|
|
START: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_("Start time for the time constraint. "
|
|
"A CRON expression property."),
|
|
constraints=[
|
|
constraints.CustomConstraint(
|
|
'cron_expression')
|
|
],
|
|
required=True
|
|
),
|
|
TIME_CONSTRAINT_DESCRIPTION: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_("Description for the time constraint."),
|
|
),
|
|
DURATION: properties.Schema(
|
|
properties.Schema.INTEGER,
|
|
_("Duration for the time constraint."),
|
|
constraints=[
|
|
constraints.Range(min=0)
|
|
],
|
|
required=True
|
|
),
|
|
TIMEZONE: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_("Timezone for the time constraint "
|
|
"(eg. 'Taiwan/Taipei', 'Europe/Amsterdam')."),
|
|
constraints=[
|
|
constraints.CustomConstraint('timezone')
|
|
],
|
|
)
|
|
}
|
|
|
|
),
|
|
support_status=support.SupportStatus(version='5.0.0'),
|
|
default=[],
|
|
)
|
|
}
|
|
|
|
|
|
NOVA_METERS = ['instance', 'memory', 'memory.usage',
|
|
'cpu', 'cpu_util', 'vcpus',
|
|
'disk.read.requests', 'disk.read.requests.rate',
|
|
'disk.write.requests', 'disk.write.requests.rate',
|
|
'disk.read.bytes', 'disk.read.bytes.rate',
|
|
'disk.write.bytes', 'disk.write.bytes.rate',
|
|
'disk.device.read.requests', 'disk.device.read.requests.rate',
|
|
'disk.device.write.requests', 'disk.device.write.requests.rate',
|
|
'disk.device.read.bytes', 'disk.device.read.bytes.rate',
|
|
'disk.device.write.bytes', 'disk.device.write.bytes.rate',
|
|
'disk.root.size', 'disk.ephemeral.size',
|
|
'network.incoming.bytes', 'network.incoming.bytes.rate',
|
|
'network.outgoing.bytes', 'network.outgoing.bytes.rate',
|
|
'network.incoming.packets', 'network.incoming.packets.rate',
|
|
'network.outgoing.packets', 'network.outgoing.packets.rate']
|
|
|
|
|
|
def actions_to_urls(stack, properties):
|
|
kwargs = {}
|
|
for k, v in iter(properties.items()):
|
|
if k in [ALARM_ACTIONS, OK_ACTIONS,
|
|
INSUFFICIENT_DATA_ACTIONS] and v is not None:
|
|
kwargs[k] = []
|
|
for act in v:
|
|
# if the action is a resource name
|
|
# we ask the destination resource for an alarm url.
|
|
# the template writer should really do this in the
|
|
# template if possible with:
|
|
# {Fn::GetAtt: ['MyAction', 'AlarmUrl']}
|
|
if act in stack:
|
|
url = stack[act].FnGetAtt('AlarmUrl')
|
|
kwargs[k].append(url)
|
|
else:
|
|
if act:
|
|
kwargs[k].append(act)
|
|
else:
|
|
kwargs[k] = v
|
|
return kwargs
|
|
|
|
|
|
class CeilometerAlarm(resource.Resource):
|
|
"""A resource that implements alarming service of Ceilometer.
|
|
|
|
A resource that allows for the setting alarms based on threshold evaluation
|
|
for a collection of samples. Also, you can define actions to take if state
|
|
of watched resource will be satisfied specified conditions. For example, it
|
|
can watch for the memory consumption and when it reaches 70% on a given
|
|
instance if the instance has been up for more than 10 min, some action will
|
|
be called.
|
|
"""
|
|
|
|
PROPERTIES = (
|
|
COMPARISON_OPERATOR, EVALUATION_PERIODS, METER_NAME, PERIOD,
|
|
STATISTIC, THRESHOLD, MATCHING_METADATA, QUERY,
|
|
) = (
|
|
'comparison_operator', 'evaluation_periods', 'meter_name', 'period',
|
|
'statistic', 'threshold', 'matching_metadata', 'query',
|
|
)
|
|
|
|
QUERY_FACTOR_FIELDS = (
|
|
QF_FIELD, QF_OP, QF_VALUE,
|
|
) = (
|
|
'field', 'op', 'value',
|
|
)
|
|
|
|
QF_OP_VALS = constraints.AllowedValues(['le', 'ge', 'eq',
|
|
'lt', 'gt', 'ne'])
|
|
|
|
properties_schema = {
|
|
COMPARISON_OPERATOR: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Operator used to compare specified statistic with threshold.'),
|
|
constraints=[
|
|
constraints.AllowedValues(['ge', 'gt', 'eq', 'ne', 'lt',
|
|
'le']),
|
|
],
|
|
update_allowed=True
|
|
),
|
|
EVALUATION_PERIODS: properties.Schema(
|
|
properties.Schema.INTEGER,
|
|
_('Number of periods to evaluate over.'),
|
|
update_allowed=True
|
|
),
|
|
METER_NAME: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Meter name watched by the alarm.'),
|
|
required=True
|
|
),
|
|
PERIOD: properties.Schema(
|
|
properties.Schema.INTEGER,
|
|
_('Period (seconds) to evaluate over.'),
|
|
update_allowed=True
|
|
),
|
|
STATISTIC: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Meter statistic to evaluate.'),
|
|
constraints=[
|
|
constraints.AllowedValues(['count', 'avg', 'sum', 'min',
|
|
'max']),
|
|
],
|
|
update_allowed=True
|
|
),
|
|
THRESHOLD: properties.Schema(
|
|
properties.Schema.NUMBER,
|
|
_('Threshold to evaluate against.'),
|
|
required=True,
|
|
update_allowed=True
|
|
),
|
|
MATCHING_METADATA: properties.Schema(
|
|
properties.Schema.MAP,
|
|
_('Meter should match this resource metadata (key=value) '
|
|
'additionally to the meter_name.'),
|
|
default={},
|
|
update_allowed=True
|
|
),
|
|
QUERY: properties.Schema(
|
|
properties.Schema.LIST,
|
|
_('A list of query factors, each comparing '
|
|
'a Sample attribute with a value. '
|
|
'Implicitly combined with matching_metadata, if any.'),
|
|
update_allowed=True,
|
|
support_status=support.SupportStatus(version='2015.1'),
|
|
schema=properties.Schema(
|
|
properties.Schema.MAP,
|
|
schema={
|
|
QF_FIELD: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Name of attribute to compare. '
|
|
'Names of the form metadata.user_metadata.X '
|
|
'or metadata.metering.X are equivalent to what '
|
|
'you can address through matching_metadata; '
|
|
'the former for Nova meters, '
|
|
'the latter for all others. '
|
|
'To see the attributes of your Samples, '
|
|
'use `ceilometer --debug sample-list`.')
|
|
),
|
|
QF_OP: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Comparison operator.'),
|
|
constraints=[QF_OP_VALS]
|
|
),
|
|
QF_VALUE: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('String value with which to compare.')
|
|
)
|
|
}
|
|
)
|
|
)
|
|
}
|
|
properties_schema.update(common_properties_schema)
|
|
|
|
default_client_name = 'ceilometer'
|
|
|
|
entity = 'alarms'
|
|
|
|
def cfn_to_ceilometer(self, stack, properties):
|
|
"""Apply all relevant compatibility xforms."""
|
|
|
|
kwargs = actions_to_urls(stack, properties)
|
|
kwargs['type'] = 'threshold'
|
|
if kwargs.get(self.METER_NAME) in NOVA_METERS:
|
|
prefix = 'user_metadata.'
|
|
else:
|
|
prefix = 'metering.'
|
|
|
|
rule = {}
|
|
for field in ['period', 'evaluation_periods', 'threshold',
|
|
'statistic', 'comparison_operator', 'meter_name']:
|
|
if field in kwargs:
|
|
rule[field] = kwargs[field]
|
|
del kwargs[field]
|
|
mmd = properties.get(self.MATCHING_METADATA) or {}
|
|
query = properties.get(self.QUERY) or []
|
|
|
|
# make sure the matching_metadata appears in the query like this:
|
|
# {field: metadata.$prefix.x, ...}
|
|
for m_k, m_v in six.iteritems(mmd):
|
|
if m_k.startswith('metadata.%s' % prefix):
|
|
key = m_k
|
|
elif m_k.startswith(prefix):
|
|
key = 'metadata.%s' % m_k
|
|
else:
|
|
key = 'metadata.%s%s' % (prefix, m_k)
|
|
# NOTE(prazumovsky): type of query value must be a string, but
|
|
# matching_metadata value type can not be a string, so we
|
|
# must convert value to a string type.
|
|
query.append(dict(field=key, op='eq', value=six.text_type(m_v)))
|
|
if self.MATCHING_METADATA in kwargs:
|
|
del kwargs[self.MATCHING_METADATA]
|
|
if self.QUERY in kwargs:
|
|
del kwargs[self.QUERY]
|
|
if query:
|
|
rule['query'] = query
|
|
kwargs['threshold_rule'] = rule
|
|
return kwargs
|
|
|
|
def handle_create(self):
|
|
props = self.cfn_to_ceilometer(self.stack,
|
|
self.properties)
|
|
props['name'] = self.physical_resource_name()
|
|
alarm = self.client().alarms.create(**props)
|
|
self.resource_id_set(alarm.alarm_id)
|
|
|
|
# the watchrule below is for backwards compatibility.
|
|
# 1) so we don't create watch tasks unneccessarly
|
|
# 2) to support CW stats post, we will redirect the request
|
|
# to ceilometer.
|
|
wr = watchrule.WatchRule(context=self.context,
|
|
watch_name=self.physical_resource_name(),
|
|
rule=self.parsed_template('Properties'),
|
|
stack_id=self.stack.id)
|
|
wr.state = wr.CEILOMETER_CONTROLLED
|
|
wr.store()
|
|
|
|
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
|
if prop_diff:
|
|
kwargs = {'alarm_id': self.resource_id}
|
|
kwargs.update(self.properties)
|
|
kwargs.update(prop_diff)
|
|
alarms_client = self.client().alarms
|
|
alarms_client.update(**self.cfn_to_ceilometer(self.stack, kwargs))
|
|
|
|
def handle_suspend(self):
|
|
if self.resource_id is not None:
|
|
self.client().alarms.update(alarm_id=self.resource_id,
|
|
enabled=False)
|
|
|
|
def handle_resume(self):
|
|
if self.resource_id is not None:
|
|
self.client().alarms.update(alarm_id=self.resource_id,
|
|
enabled=True)
|
|
|
|
def handle_delete(self):
|
|
try:
|
|
wr = watchrule.WatchRule.load(
|
|
self.context, watch_name=self.physical_resource_name())
|
|
wr.destroy()
|
|
except exception.EntityNotFound:
|
|
pass
|
|
|
|
return super(CeilometerAlarm, self).handle_delete()
|
|
|
|
def handle_check(self):
|
|
watch_name = self.physical_resource_name()
|
|
watchrule.WatchRule.load(self.context, watch_name=watch_name)
|
|
self.client().alarms.get(self.resource_id)
|
|
|
|
|
|
class BaseCeilometerAlarm(resource.Resource):
|
|
default_client_name = 'ceilometer'
|
|
|
|
entity = 'alarms'
|
|
|
|
def handle_create(self):
|
|
properties = actions_to_urls(self.stack,
|
|
self.properties)
|
|
properties['name'] = self.physical_resource_name()
|
|
properties['type'] = self.ceilometer_alarm_type
|
|
alarm = self.client().alarms.create(
|
|
**self._reformat_properties(properties))
|
|
self.resource_id_set(alarm.alarm_id)
|
|
|
|
def _reformat_properties(self, properties):
|
|
rule = {}
|
|
for name in self.PROPERTIES:
|
|
value = properties.pop(name, None)
|
|
if value:
|
|
rule[name] = value
|
|
if rule:
|
|
properties['%s_rule' % self.ceilometer_alarm_type] = rule
|
|
return properties
|
|
|
|
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
|
|
if prop_diff:
|
|
kwargs = {'alarm_id': self.resource_id}
|
|
kwargs.update(prop_diff)
|
|
alarms_client = self.client().alarms
|
|
alarms_client.update(**self._reformat_properties(
|
|
actions_to_urls(self.stack, kwargs)))
|
|
|
|
def handle_suspend(self):
|
|
self.client().alarms.update(
|
|
alarm_id=self.resource_id, enabled=False)
|
|
|
|
def handle_resume(self):
|
|
self.client().alarms.update(
|
|
alarm_id=self.resource_id, enabled=True)
|
|
|
|
def handle_check(self):
|
|
self.client().alarms.get(self.resource_id)
|
|
|
|
|
|
class CombinationAlarm(BaseCeilometerAlarm):
|
|
"""A resource that implements combination of Ceilometer alarms.
|
|
|
|
Allows to use alarm as a combination of other alarms with some operator:
|
|
activate this alarm if any alarm in combination has been activated or
|
|
if all alarms in combination have been activated.
|
|
"""
|
|
|
|
support_status = support.SupportStatus(version='2014.1')
|
|
|
|
PROPERTIES = (
|
|
ALARM_IDS, OPERATOR,
|
|
) = (
|
|
'alarm_ids', 'operator',
|
|
)
|
|
|
|
properties_schema = {
|
|
ALARM_IDS: properties.Schema(
|
|
properties.Schema.LIST,
|
|
_('List of alarm identifiers to combine.'),
|
|
required=True,
|
|
constraints=[constraints.Length(min=1)],
|
|
update_allowed=True),
|
|
OPERATOR: properties.Schema(
|
|
properties.Schema.STRING,
|
|
_('Operator used to combine the alarms.'),
|
|
constraints=[constraints.AllowedValues(['and', 'or'])],
|
|
update_allowed=True)
|
|
}
|
|
properties_schema.update(common_properties_schema)
|
|
|
|
ceilometer_alarm_type = 'combination'
|
|
|
|
|
|
def resource_mapping():
|
|
return {
|
|
'OS::Ceilometer::Alarm': CeilometerAlarm,
|
|
'OS::Ceilometer::CombinationAlarm': CombinationAlarm,
|
|
}
|