From ba39188b2f314f1297d2591bb03c75f06e653c6b Mon Sep 17 00:00:00 2001 From: Deklan Dieterly Date: Wed, 29 Oct 2014 16:14:39 -0600 Subject: [PATCH] Add alarm definition create resource Change-Id: I65e1c9f8697632e3c2004282bb624ed814828864 --- etc/monasca.conf | 3 + monasca/api/alarm_definitions_api_v2.py | 48 +++ monasca/api/monasca_api_v2.py | 24 -- monasca/api/monasca_transforms_api_v2.py | 5 +- monasca/api/server.py | 154 +++++----- .../Alarm_Definitions_Repository.py | 25 ++ .../mysql/alarm_definitions_repository.py | 152 +++++++++ monasca/common/resource_api.py | 9 +- monasca/dispatcher/kafka_dispatcher.py | 2 +- monasca/expression_parser/__init__.py | 0 .../expression_parser/alarm_expr_parser.py | 243 +++++++++++++++ monasca/middleware/mock_auth_filter.py | 2 + monasca/tests/__init__.py | 2 +- .../alarm_definition_request_body_schema.py | 46 +++ .../v2/common/schemas/dimensions_schema.py | 6 +- .../schemas/events_request_body_schema.py | 6 +- monasca/v2/common/schemas/exceptions.py | 3 +- .../v2/common/schemas/metric_name_schema.py | 2 +- .../schemas/metrics_request_body_schema.py | 2 +- .../schemas/transforms_request_body_schema.py | 5 +- monasca/v2/common/utils.py | 3 +- monasca/v2/reference/alarm_definitions.py | 289 ++++++++++++++++++ monasca/v2/reference/events.py | 36 ++- monasca/v2/reference/helpers.py | 46 ++- monasca/v2/reference/metrics.py | 72 +++-- monasca/v2/reference/notifications.py | 99 +++--- monasca/v2/reference/transforms.py | 44 +-- setup.cfg | 4 + 28 files changed, 1075 insertions(+), 257 deletions(-) create mode 100644 monasca/api/alarm_definitions_api_v2.py create mode 100644 monasca/common/repositories/Alarm_Definitions_Repository.py create mode 100644 monasca/common/repositories/mysql/alarm_definitions_repository.py create mode 100644 monasca/expression_parser/__init__.py create mode 100644 monasca/expression_parser/alarm_expr_parser.py create mode 100644 monasca/v2/common/schemas/alarm_definition_request_body_schema.py create mode 100644 monasca/v2/reference/alarm_definitions.py diff --git a/etc/monasca.conf b/etc/monasca.conf index 5d7e9dfe0..0b08dde34 100644 --- a/etc/monasca.conf +++ b/etc/monasca.conf @@ -39,6 +39,9 @@ events_driver = none # The driver to use for the transforms repository transforms_driver = mysql_transforms_repo +# The driver to use for the alarm definitions repository +alarm_definitions_driver = mysql_alarm_definitions_repo + # The driver to use for the notifications repository notifications_driver = mysql_notifications_repo diff --git a/monasca/api/alarm_definitions_api_v2.py b/monasca/api/alarm_definitions_api_v2.py new file mode 100644 index 000000000..66fdd9f3d --- /dev/null +++ b/monasca/api/alarm_definitions_api_v2.py @@ -0,0 +1,48 @@ +# Copyright 2014 Hewlett-Packard +# +# 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 monasca.common import resource_api +from monasca.openstack.common import log + +LOG = log.getLogger(__name__) + + +class AlarmDefinitionsV2API(object): + + def __init__(self, global_conf): + LOG.debug('initializing AlarmDefinitionsV2API!') + self.global_conf = global_conf + + @resource_api.Restify('/v2.0/alarm-definitions', method='post') + def do_post_alarm_definitions(self, req, res): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='get') + def do_get_alarm_definition(self, req, res, id): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='put') + def do_put_alarm_definitions(self, req, res, id): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions', method='get') + def do_get_alarm_definitions(self, req, res): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='patch') + def do_patch_alarm_definitions(self, req, res, id): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='delete') + def do_delete_alarm_definitions(self, req, res, id): + res.status = '501 Not Implemented' diff --git a/monasca/api/monasca_api_v2.py b/monasca/api/monasca_api_v2.py index 9fb16f171..3e1cd11d4 100644 --- a/monasca/api/monasca_api_v2.py +++ b/monasca/api/monasca_api_v2.py @@ -46,30 +46,6 @@ class V2API(object): def do_get_statistics(self, req, res): res.status = '501 Not Implemented' - @resource_api.Restify('/v2.0/alarm-definitions', method='post') - def do_post_alarm_definitions(self, req, res): - res.status = '501 Not Implemented' - - @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='get') - def do_get_alarm_definition(self, req, res, id): - res.status = '501 Not Implemented' - - @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='put') - def do_put_alarm_definitions(self, req, res, id): - res.status = '501 Not Implemented' - - @resource_api.Restify('/v2.0/alarm-definitions', method='get') - def do_get_alarm_definitions(self, req, res): - res.status = '501 Not Implemented' - - @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='patch') - def do_patch_alarm_definitions(self, req, res, id): - res.status = '501 Not Implemented' - - @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='delete') - def do_delete_alarm_definitions(self, req, res, id): - res.status = '501 Not Implemented' - @resource_api.Restify('/v2.0/alarms/{id}', method='put') def do_put_alarms(self, req, res, id): res.status = '501 Not Implemented' diff --git a/monasca/api/monasca_transforms_api_v2.py b/monasca/api/monasca_transforms_api_v2.py index a693beec5..99c03a5a2 100644 --- a/monasca/api/monasca_transforms_api_v2.py +++ b/monasca/api/monasca_transforms_api_v2.py @@ -4,7 +4,7 @@ # 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 +# 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 @@ -32,6 +32,7 @@ class TransformsV2API(object): def do_get_transforms(self, req, res): res.status = '501 Not Implemented' - @resource_api.Restify('/v2.0/events/transforms/{transform_id}', method='delete') + @resource_api.Restify('/v2.0/events/transforms/{transform_id}', + method='delete') def do_delete_transforms(self, req, res, transform_id): res.status = '501 Not Implemented' diff --git a/monasca/api/server.py b/monasca/api/server.py index 9b5481ace..77824e12d 100644 --- a/monasca/api/server.py +++ b/monasca/api/server.py @@ -6,7 +6,7 @@ # 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 +# 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 @@ -24,49 +24,56 @@ from paste.deploy import loadapp from wsgiref import simple_server METRICS_DISPATCHER_NAMESPACE = 'monasca.metrics_dispatcher' +ALARM_DEFINITIONS_DISPATCHER_NAMESPACE = 'monasca.alarm_definitions_dispatcher' EVENTS_DISPATCHER_NAMESPACE = 'monasca.events_dispatcher' TRANSFORMS_DISPATCHER_NAMESPACE = 'monasca.transforms_dispatcher' NOTIFICATIONS_DISPATCHER_NAMESPACE = 'monasca.notifications_dispatcher' LOG = log.getLogger(__name__) -global_opts = [ - cfg.StrOpt('region', help='Region that API is running in') -] +global_opts = [cfg.StrOpt('region', help='Region that API is running in')] cfg.CONF.register_opts(global_opts) -security_opts = [ - cfg.ListOpt('default_authorized_roles', default=['admin'], - help='Roles that are allowed full access to the API'), - cfg.ListOpt('agent_authorized_roles', default=['agent'], - help='Roles that are only allowed to POST to the API'), - cfg.ListOpt('delegate_authorized_roles', default=['admin'], - help='Roles that are allowed to POST metrics on behalf of another tenant') -] +security_opts = [cfg.ListOpt('default_authorized_roles', default=['admin'], + help='Roles that are allowed full access to the ' + 'API'), + cfg.ListOpt('agent_authorized_roles', default=['agent'], + help='Roles that are only allowed to POST to ' + 'the API'), + cfg.ListOpt('delegate_authorized_roles', default=['admin'], + help='Roles that are allowed to POST metrics on ' + 'behalf of another tenant')] security_group = cfg.OptGroup(name='security', title='security') cfg.CONF.register_group(security_group) cfg.CONF.register_opts(security_opts, security_group) -messaging_opts = [ - cfg.StrOpt('driver', default='kafka', help='The message queue driver to use'), - cfg.StrOpt('metrics_message_format', default='reference', - help='The type of metrics message format to publish to the message queue'), - cfg.StrOpt('events_message_format', default='reference', - help='The type of events message format to publish to the message queue') -] +messaging_opts = [cfg.StrOpt('driver', default='kafka', + help='The message queue driver to use'), + cfg.StrOpt('metrics_message_format', default='reference', + help='The type of metrics message format to ' + 'publish to the message queue'), + cfg.StrOpt('events_message_format', default='reference', + help='The type of events message format to ' + 'publish to the message queue')] messaging_group = cfg.OptGroup(name='messaging', title='messaging') cfg.CONF.register_group(messaging_group) cfg.CONF.register_opts(messaging_opts, messaging_group) repositories_opts = [ - cfg.StrOpt('metrics_driver', default='influxdb_metrics_repo', help='The repository driver to use for metrics'), - cfg.StrOpt('events_driver', default='fake_events_repo', help='The repository driver to use for events'), - cfg.StrOpt('transforms_driver', default='mysql_transforms_repo', help='The repository driver to use for transforms'), - cfg.StrOpt('notifications_driver', default='mysql_notifications_repo', help='The repository driver to use for notifications') -] + cfg.StrOpt('metrics_driver', default='influxdb_metrics_repo', + help='The repository driver to use for metrics'), + cfg.StrOpt('alarm_definitions_driver', + default='mysql_alarm_definitions_repo', + help='The repository driver to use for alarm definitions'), + cfg.StrOpt('events_driver', default='fake_events_repo', + help='The repository driver to use for events'), + cfg.StrOpt('transforms_driver', default='mysql_transforms_repo', + help='The repository driver to use for transforms'), + cfg.StrOpt('notifications_driver', default='mysql_notifications_repo', + help='The repository driver to use for notifications')] repositories_group = cfg.OptGroup(name='repositories', title='repositories') cfg.CONF.register_group(repositories_group) @@ -74,78 +81,58 @@ cfg.CONF.register_opts(repositories_opts, repositories_group) dispatcher_opts = [ cfg.StrOpt('driver', default='monasca.v2.reference.metrics:Metrics', - help='The name of the dispatcher for the api server') -] + help='The name of the dispatcher for the api server')] dispatcher_group = cfg.OptGroup(name='dispatcher', title='dispatcher') cfg.CONF.register_group(dispatcher_group) cfg.CONF.register_opts(dispatcher_opts, dispatcher_group) -kafka_opts = [ - cfg.StrOpt('uri', - help='Address to kafka server. For example: ' - 'uri=192.168.1.191:9092'), - cfg.StrOpt('metrics_topic', - default='metrics', - help='The topic that metrics will be published too.'), - cfg.StrOpt('events_topic', - default='raw-events', - help='The topic that events will be published too.'), - cfg.StrOpt('group', - default='api', - help='The group name that this service belongs to.'), - cfg.IntOpt('wait_time', - default=1, - help='The wait time when no messages on kafka queue.'), - cfg.IntOpt('ack_time', - default=20, - help='The ack time back to kafka.'), - cfg.IntOpt('max_retry', - default=3, - help='The number of retry when there is a connection error.'), - cfg.BoolOpt('auto_commit', - default=False, - help='If automatically commmit when consume messages.'), - cfg.BoolOpt('async', - default=True, - help='The type of posting.'), - cfg.BoolOpt('compact', - default=True, - help=('Specify if the message received should be parsed.' - 'If True, message will not be parsed, otherwise ' - 'messages will be parsed.')), - cfg.MultiOpt('partitions', - item_type=types.Integer(), - default=[0], - help='The sleep time when no messages on kafka queue.'), - cfg.BoolOpt('drop_data', - default=False, - help=('Specify if received data should be simply dropped. ' - 'This parameter is only for testing purposes.')), -] +kafka_opts = [cfg.StrOpt('uri', help='Address to kafka server. For example: ' + 'uri=192.168.1.191:9092'), + cfg.StrOpt('metrics_topic', default='metrics', + help='The topic that metrics will be published too.'), + cfg.StrOpt('events_topic', default='raw-events', + help='The topic that events will be published too.'), + cfg.StrOpt('group', default='api', + help='The group name that this service belongs to.'), + cfg.IntOpt('wait_time', default=1, + help='The wait time when no messages on kafka ' + 'queue.'), + cfg.IntOpt('ack_time', default=20, + help='The ack time back to kafka.'), + cfg.IntOpt('max_retry', default=3, + help='The number of retry when there is a ' + 'connection error.'), + cfg.BoolOpt('auto_commit', default=False, + help='If automatically commmit when consume ' + 'messages.'), + cfg.BoolOpt('async', default=True, help='The type of posting.'), + cfg.BoolOpt('compact', default=True, help=( + 'Specify if the message received should be parsed.' + 'If True, message will not be parsed, otherwise ' + 'messages will be parsed.')), + cfg.MultiOpt('partitions', item_type=types.Integer(), + default=[0], + help='The sleep time when no messages on kafka ' + 'queue.'), + cfg.BoolOpt('drop_data', default=False, help=( + 'Specify if received data should be simply dropped. ' + 'This parameter is only for testing purposes.')), ] kafka_group = cfg.OptGroup(name='kafka', title='title') cfg.CONF.register_group(kafka_group) cfg.CONF.register_opts(kafka_opts, kafka_group) -influxdb_opts = [ - cfg.StrOpt('database_name'), - cfg.StrOpt('ip_address'), - cfg.StrOpt('port'), - cfg.StrOpt('user'), - cfg.StrOpt('password') -] +influxdb_opts = [cfg.StrOpt('database_name'), cfg.StrOpt('ip_address'), + cfg.StrOpt('port'), cfg.StrOpt('user'), + cfg.StrOpt('password')] influxdb_group = cfg.OptGroup(name='influxdb', title='influxdb') cfg.CONF.register_group(influxdb_group) cfg.CONF.register_opts(influxdb_opts, influxdb_group) -mysql_opts = [ - cfg.StrOpt('database_name'), - cfg.StrOpt('hostname'), - cfg.StrOpt('username'), - cfg.StrOpt('password') -] +mysql_opts = [cfg.StrOpt('database_name'), cfg.StrOpt('hostname'), + cfg.StrOpt('username'), cfg.StrOpt('password')] mysql_group = cfg.OptGroup(name='mysql', title='mysql') cfg.CONF.register_group(mysql_group) @@ -179,6 +166,11 @@ def api_app(conf): app.add_resource('notifications', NOTIFICATIONS_DISPATCHER_NAMESPACE, cfg.CONF.dispatcher.driver, [conf]) + # load the alarm definitions resource + app.add_resource('alarm-definitions', + ALARM_DEFINITIONS_DISPATCHER_NAMESPACE, + cfg.CONF.dispatcher.driver, [conf]) + return app diff --git a/monasca/common/repositories/Alarm_Definitions_Repository.py b/monasca/common/repositories/Alarm_Definitions_Repository.py new file mode 100644 index 000000000..dacbd49e5 --- /dev/null +++ b/monasca/common/repositories/Alarm_Definitions_Repository.py @@ -0,0 +1,25 @@ +# Copyright 2014 Hewlett-Packard +# +# 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 six + + +@six.add_metaclass(abc.ABCMeta) +class AlarmDefinitionsRepository(object): + + @abc.abstractmethod + def create_alarm_definition(self, tenant_id, name, + expression, sub_expr_list, description, severity, match_by, alarm_actions, + undetermined_actions, ok_action): + pass diff --git a/monasca/common/repositories/mysql/alarm_definitions_repository.py b/monasca/common/repositories/mysql/alarm_definitions_repository.py new file mode 100644 index 000000000..4c55cc2bd --- /dev/null +++ b/monasca/common/repositories/mysql/alarm_definitions_repository.py @@ -0,0 +1,152 @@ +# Copyright 2014 Hewlett-Packard +# +# 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 datetime +import pyodbc + +from oslo.config import cfg + +from monasca.common.repositories import alarm_definitions_repository +from monasca.openstack.common import log +from monasca.openstack.common import uuidutils +from monasca.common.repositories import exceptions + + +LOG = log.getLogger(__name__) + + +class AlarmDefinitionsRepository( + alarm_definitions_repository.AlarmDefinitionsRepository): + database_driver = 'MySQL ODBC 5.3 Unicode Driver' + database_cnxn_template = 'DRIVER={' \ + '%s};Server=%s;CHARSET=UTF8;Database=%s;Uid=%s' \ + ';Pwd=%s' + + def __init__(self): + + try: + self.conf = cfg.CONF + database_name = self.conf.mysql.database_name + database_server = self.conf.mysql.hostname + database_uid = self.conf.mysql.username + database_pwd = self.conf.mysql.password + self._cnxn_string = ( + AlarmDefinitionsRepository.database_cnxn_template % ( + AlarmDefinitionsRepository.database_driver, + database_server, database_name, database_uid, + database_pwd)) + + except Exception as ex: + LOG.exception(ex) + raise exceptions.RepositoryException(ex) + + + def create_alarm_definition(self, tenant_id, name, expression, + sub_expr_list, description, severity, match_by, + alarm_actions, undetermined_actions, + ok_actions): + + try: + cnxn = pyodbc.connect(self._cnxn_string) + cursor = cnxn.cursor() + now = datetime.datetime.utcnow() + alarm_definition_id = uuidutils.generate_uuid() + cursor.execute("insert into alarm_definition(" + "id, " + "tenant_id, " + "name, " + "description, " + "expression, " + "severity, " + "match_by," + "actions_enabled, " + "created_at, " + "updated_at) " + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + alarm_definition_id, tenant_id, name.encode('utf8'), + description.encode('utf8'), + expression.encode('utf8'), + severity.upper().encode('utf8'), + ",".join(match_by).encode('utf8'), 1, now, now) + + for sub_expr in sub_expr_list: + sub_alarm_definition_id = uuidutils.generate_uuid() + sub_expr.id = sub_alarm_definition_id + cursor.execute("insert into sub_alarm_definition(" + "id, " + "alarm_definition_id," + "function, " + "metric_name, " + "operator, " + "threshold," + "period, " + "periods, " + "created_at, " + "updated_at)" + " values(?,?,?,?,?,?,?,?,?,?)", + sub_alarm_definition_id, alarm_definition_id, + sub_expr.get_normalized_func().encode('utf8'), + sub_expr.get_normalized_metric_name().encode( + "utf8"), + sub_expr.get_normalized_operator().encode( + 'utf8'), + sub_expr.get_threshold().encode('utf8'), + sub_expr.get_period().encode('utf8'), + sub_expr.get_periods().encode('utf8'), now, now) + + for dimension in sub_expr.get_dimensions_as_list(): + parsed_dimension = dimension.split('=') + cursor.execute( + "insert into sub_alarm_definition_dimension(" + "sub_alarm_definition_id," + "dimension_name," + "value)" + "values(?,?,?)", sub_alarm_definition_id, + parsed_dimension[0].encode('utf8'), + parsed_dimension[1].encode('utf8')) + + self._insert_into_alarm_action(cursor, alarm_definition_id, + alarm_actions, u"ALARM") + self._insert_into_alarm_action(cursor, alarm_definition_id, + undetermined_actions, + u"UNDETERMINED") + self._insert_into_alarm_action(cursor, alarm_definition_id, + ok_actions, u"OK") + + cnxn.commit() + cnxn.close() + + return alarm_definition_id + + except Exception as ex: + LOG.exception(ex) + raise exceptions.RepositoryException(ex) + + + def _insert_into_alarm_action(self, cursor, alarm_definition_id, actions, + alarm_state): + for action in actions: + cursor.execute("select id from notification_method where id = ?", + action.encode('utf8')) + row = cursor.fetchone() + if not row: + raise exceptions.RepositoryException( + "Non-existent notification id {} submitted for {} " + "notification action".format(action.encode('utf8'), + alarm_state.encode('utf8'))) + cursor.execute("insert into alarm_action(" + "alarm_id," + "alarm_state," + "action_id)" + "values(?,?,?)", alarm_definition_id, + alarm_state.encode('utf8'), action.encode('utf8')) diff --git a/monasca/common/resource_api.py b/monasca/common/resource_api.py index eeed252f2..a49c30b6e 100644 --- a/monasca/common/resource_api.py +++ b/monasca/common/resource_api.py @@ -25,21 +25,18 @@ RESOURCE_METHOD_FLAG = 'fab05a04-b861-4651-bd0c-9cb3eb9a6088' LOG = log.getLogger(__name__) -def init_driver(namespace, driver_name, drv_invoke_args=None): +def init_driver(namespace, driver_name, drv_invoke_args=()): """Initialize the resource driver and returns it. :param namespace: the resource namespace (in setup.cfg). :param driver_name: the driver name (in monasca.conf) :param invoke_args: args to pass to the driver (a tuple) """ - invoke_args_tuple = () - if drv_invoke_args: - invoke_args_tuple = drv_invoke_args mgr = driver.DriverManager( namespace = namespace, name = driver_name, invoke_on_load = True, - invoke_args = invoke_args_tuple + invoke_args = drv_invoke_args ) return mgr.driver @@ -135,7 +132,7 @@ class ResourceAPI(falcon.API): LOG.debug(self._routes) def add_resource(self, resource_name, namespace, driver_name, - invoke_args=None, uri=None): + invoke_args=(), uri=None): """Loads the resource driver, and adds it to the routes. :param resource_name: the name of the resource. diff --git a/monasca/dispatcher/kafka_dispatcher.py b/monasca/dispatcher/kafka_dispatcher.py index 811c584c7..13d624965 100644 --- a/monasca/dispatcher/kafka_dispatcher.py +++ b/monasca/dispatcher/kafka_dispatcher.py @@ -38,4 +38,4 @@ class KafkaDispatcher(monasca_api_v2.V2API): msg = req.stream.read() code = self._kafka_conn.send_messages(msg) - res.status = getattr(falcon, 'HTTP_' + str(code)) \ No newline at end of file + res.status = getattr(falcon, 'HTTP_' + str(code)) diff --git a/monasca/expression_parser/__init__.py b/monasca/expression_parser/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monasca/expression_parser/alarm_expr_parser.py b/monasca/expression_parser/alarm_expr_parser.py new file mode 100644 index 000000000..db41d992c --- /dev/null +++ b/monasca/expression_parser/alarm_expr_parser.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2014 Hewlett-Packard +# +# 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 itertools +import sys + +from pyparsing import CaselessLiteral +from pyparsing import alphanums +from pyparsing import delimitedList +from pyparsing import Forward +from pyparsing import Group +from pyparsing import Literal +from pyparsing import nums +from pyparsing import opAssoc +from pyparsing import operatorPrecedence +from pyparsing import Optional +from pyparsing import stringEnd +from pyparsing import Word + + +class SubExpr(object): + def __init__(self, tokens): + self.sub_expr = tokens + self.func = tokens.func + self.metric_name = tokens.metric_name + self.dimensions = tokens.dimensions.dimensions_list + self.operator = tokens.relational_op + self.threshold = tokens.threshold + self.period = tokens.period + self.periods = tokens.periods + + def get_sub_expr_str(self): + return "".join(list(itertools.chain(*self.sub_expr))) + + def get_fmtd_sub_expr(self): + + result = "{}({}".format(self.func.encode('utf8'), + self.metric_name.encode('utf8')) + + if self.dimensions: + result += "{{{}}}".format(self.dimensions.encode('utf8')) + + if self.period: + result += ", {}".format(self.period.encode('utf8')) + + result += ")" + + result += " {} {}".format(self.operator.encode('utf8'), + self.threshold.encode('utf8')) + + if self.periods: + result += " times {}".format(self.periods.encode('utf8')) + + return result.decode('utf8') + + def get_dimensions_str(self): + return self.dimensions + + def get_operands_list(self): + return [self] + + def get_func(self): + return self.func + + def get_normalized_func(self): + return self.func.upper() + + def get_metric_name(self): + return self.metric_name + + def get_normalized_metric_name(self): + return self.metric_name.lower() + + def get_dimensions(self): + return self.dimensions + + def get_dimensions_as_list(self): + if self.dimensions: + return self.dimensions.split(",") + else: + return [] + + def get_operator(self): + return self.operator + + def get_threshold(self): + return self.threshold + + def get_period(self): + if self.period: + return self.period + else: + return u'60' + + def get_periods(self): + if self.periods: + return self.periods + else: + return u'1' + + def get_normalized_operator(self): + if self.operator.lower() == "lt" or self.operator == "<": + return u"LT" + elif self.operator.lower() == "gt" or self.operator == ">": + return u"GT" + elif self.operator.lower() == "lte" or self.operator == "<=": + return u"LTE" + elif self.operator.lower() == "gte" or self.operator == ">=": + return u"GTE" + + +class BinaryOp(object): + def __init__(self, tokens): + self.op = tokens[0][1] + self.operands = tokens[0][0::2] + + def get_operands_list(self): + return ([sub_operand for operand in self.operands for sub_operand in + operand.get_operands_list()]) + + +class AndSubExpr(BinaryOp): + """ Expand later as needed. + """ + pass + + +class OrSubExpr(BinaryOp): + """Expand later as needed. + """ + pass + + +COMMA = Literal(",") +LPAREN = Literal("(") +RPAREN = Literal(")") +EQUAL = Literal("=") +LBRACE = Literal("{") +RBRACE = Literal("}") + +# Initialize non-ascii unicode code points in the Basic Multilingual Plane. +unicode_printables = u''.join( + unichr(c) for c in xrange(128, 65536) if not unichr(c).isspace()) + +# Does not like comma. No Literals from above allowed. +valid_identifier_chars = (unicode_printables + alphanums + ".-_#!$%&'*+/:;?@[" + "\\]^`|~") + +metric_name = Word(valid_identifier_chars, min=1, max=255)("metric_name") +dimension_name = Word(valid_identifier_chars, min=1, max=255) +dimension_value = Word(valid_identifier_chars, min=1, max=255) + +integer_number = Word(nums) +decimal_number = Word(nums + ".") + +max = CaselessLiteral("max") +min = CaselessLiteral("min") +avg = CaselessLiteral("avg") +count = CaselessLiteral("count") +sum = CaselessLiteral("sum") +func = (max | min | avg | count | sum)("func") + +less_than_op = (CaselessLiteral("<") | CaselessLiteral("lt")) +less_than_eq_op = (CaselessLiteral("<=") | CaselessLiteral("lte")) +greater_than_op = (CaselessLiteral(">") | CaselessLiteral("gt")) +greater_than_eq_op = (CaselessLiteral(">=") | CaselessLiteral("gte")) + +# Order is important. Put longer prefix first. +relational_op = ( + less_than_eq_op | less_than_op | greater_than_eq_op | greater_than_op)( + "relational_op") + +AND = CaselessLiteral("and") | CaselessLiteral("&&") +OR = CaselessLiteral("or") | CaselessLiteral("||") +logical_op = (AND | OR)("logical_op") + +times = CaselessLiteral("times") + +dimension = Group(dimension_name + EQUAL + dimension_value) +dimension_list = Group(Optional( + LBRACE + delimitedList(dimension, delim=",", combine=True)( + "dimensions_list") + RBRACE)) + +metric = metric_name + dimension_list("dimensions") +period = integer_number("period") +threshold = decimal_number("threshold") +periods = integer_number("periods") + +expression = Forward() + +sub_expression = (func + LPAREN + metric + Optional( + COMMA + period) + RPAREN + relational_op + threshold + Optional( + times + periods) | LPAREN + expression + RPAREN) + +sub_expression.setParseAction(SubExpr) + +expression = operatorPrecedence(sub_expression, + [(AND, 2, opAssoc.LEFT, AndSubExpr), + (OR, 2, opAssoc.LEFT, OrSubExpr)]) + + +class AlarmExprParser(object): + def __init__(self, expr): + self._expr = expr + + def get_sub_expr_list(self): + parseResult = (expression + stringEnd).parseString(self._expr) + sub_expr_list = parseResult[0].get_operands_list() + return sub_expr_list + + +def main(): + """ Used for development and testing. + + :return: + """ + expr = "max(-_.千幸福的笑脸{घोड़ा=馬,dn2=dv2}, 60) gte 100 times 3 and " \ + "(min(ເຮືອນ{dn3=dv3,家=дом}) < 10 or sum(biz{dn5=dv5}) > 99 and " \ + "count(fizzle) lt 0 or count(baz) > 1)".decode('utf8') + # expr = "max(foo{hostname=mini-mon,千=千}, 120) > 100 and (max(bar)>100 \ + # or max(biz)>100)".decode('utf8') + alarmExprParser = AlarmExprParser(expr) + r = alarmExprParser.get_sub_expr_list() + for sub_expression in r: + print sub_expression.get_sub_expr_str() + print sub_expression.get_fmtd_sub_expr() + print sub_expression.get_dimensions_str() + print + +if __name__ == "__main__": + sys.exit(main()) diff --git a/monasca/middleware/mock_auth_filter.py b/monasca/middleware/mock_auth_filter.py index f602755d5..01460bcbf 100644 --- a/monasca/middleware/mock_auth_filter.py +++ b/monasca/middleware/mock_auth_filter.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. + class MockAuthFilter(object): ''' This authorization filter doesn't do any authentication, it just copies the @@ -27,6 +28,7 @@ class MockAuthFilter(object): env['HTTP_X_ROLES'] = 'admin' return self.app(env, start_response) + def filter_factory(global_conf, **local_conf): def validator_filter(app): return MockAuthFilter(app, local_conf) diff --git a/monasca/tests/__init__.py b/monasca/tests/__init__.py index f67f4d817..6e1cd6c21 100644 --- a/monasca/tests/__init__.py +++ b/monasca/tests/__init__.py @@ -84,4 +84,4 @@ class BaseTestCase(testtools.TestCase): os.write(fd, contents) finally: os.close(fd) - return tempfiles \ No newline at end of file + return tempfiles diff --git a/monasca/v2/common/schemas/alarm_definition_request_body_schema.py b/monasca/v2/common/schemas/alarm_definition_request_body_schema.py new file mode 100644 index 000000000..1e2117904 --- /dev/null +++ b/monasca/v2/common/schemas/alarm_definition_request_body_schema.py @@ -0,0 +1,46 @@ +# Copyright 2014 Hewlett-Packard +# +# 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 voluptuous import Schema, Length, Optional +from voluptuous import Required, Any, All + +from monasca.openstack.common import log +from monasca.v2.common.schemas import exceptions + + +LOG = log.getLogger(__name__) + +alarm_definition_schema = { + Required('name'): All(Any(str, unicode), Length(max=250)), + Required('expression'): All(Any(str, unicode), Length(max=4096)), + Optional('description'): All(Any(str, unicode), Length(max=250)), + Optional('severity'): All( + Any('low', 'medium', 'high', 'critical', 'LOW', "MEDIUM", 'HIGH', + 'CRITICAL')), + Optional('match_by'): All(Any([unicode], [str]), Length(max=255)), + Optional('ok_actions'): All(Any([str], [unicode]), Length(max=400)), + Optional('alarm_actions'): All(Any([str], [unicode]), Length(max=400)), + Optional('undetermined_actions'): All(Any([str], [unicode]), + Length(max=400))} + +request_body_schema = Schema(alarm_definition_schema, required=True, + extra=True) + + +def validate(msg): + try: + request_body_schema(msg) + except Exception as ex: + LOG.debug(ex) + raise exceptions.ValidationException(str(ex)) diff --git a/monasca/v2/common/schemas/dimensions_schema.py b/monasca/v2/common/schemas/dimensions_schema.py index 4e21ca547..9e120dc8c 100644 --- a/monasca/v2/common/schemas/dimensions_schema.py +++ b/monasca/v2/common/schemas/dimensions_schema.py @@ -19,8 +19,8 @@ from monasca.v2.common.schemas import exceptions LOG = log.getLogger(__name__) -# TODO: Add regex to validate dimension names don't use any excluded characters. -dimensions_schema = Schema({All(Any(str, unicode), Length(max=255)): All(Any(str, unicode), Length(max=255))}) +dimensions_schema = Schema({All(Any(str, unicode), Length(max=255)): + All(Any(str, unicode), Length(max=255))}) def validate(dimensions): @@ -28,4 +28,4 @@ def validate(dimensions): dimensions_schema(dimensions) except Exception as ex: LOG.debug(ex) - raise exceptions.ValidationException(str(ex)) \ No newline at end of file + raise exceptions.ValidationException(str(ex)) diff --git a/monasca/v2/common/schemas/events_request_body_schema.py b/monasca/v2/common/schemas/events_request_body_schema.py index 75614f8a0..8cd6b39cb 100644 --- a/monasca/v2/common/schemas/events_request_body_schema.py +++ b/monasca/v2/common/schemas/events_request_body_schema.py @@ -20,7 +20,9 @@ from monasca.v2.common.schemas import exceptions LOG = log.getLogger(__name__) # TODO: Add regex to validate key/values don't use any excluded characters. -event_schema_request_body = Schema({All(Any(str, unicode), Length(max=255)): All(Any(None, str, unicode, bool, int, float, dict, []))}) +event_schema_request_body = Schema({All(Any(str, unicode), Length(max=255)): + All(Any(None, str, unicode, bool, int, + float, dict, []))}) def validate(body): @@ -28,4 +30,4 @@ def validate(body): event_schema_request_body(body) except Exception as ex: LOG.debug(ex) - raise exceptions.ValidationException(str(ex)) \ No newline at end of file + raise exceptions.ValidationException(str(ex)) diff --git a/monasca/v2/common/schemas/exceptions.py b/monasca/v2/common/schemas/exceptions.py index 7638cde2e..cc20e12b2 100644 --- a/monasca/v2/common/schemas/exceptions.py +++ b/monasca/v2/common/schemas/exceptions.py @@ -12,5 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. + class ValidationException(Exception): - pass \ No newline at end of file + pass diff --git a/monasca/v2/common/schemas/metric_name_schema.py b/monasca/v2/common/schemas/metric_name_schema.py index 997011555..392c5549a 100644 --- a/monasca/v2/common/schemas/metric_name_schema.py +++ b/monasca/v2/common/schemas/metric_name_schema.py @@ -27,4 +27,4 @@ def validate(name): metric_name_schema(name) except Exception as ex: LOG.debug(ex) - raise exceptions.ValidationException(str(ex)) \ No newline at end of file + raise exceptions.ValidationException(str(ex)) diff --git a/monasca/v2/common/schemas/metrics_request_body_schema.py b/monasca/v2/common/schemas/metrics_request_body_schema.py index 1d420b266..eb4b4f997 100644 --- a/monasca/v2/common/schemas/metrics_request_body_schema.py +++ b/monasca/v2/common/schemas/metrics_request_body_schema.py @@ -36,4 +36,4 @@ def validate(msg): request_body_schema(msg) except Exception as ex: LOG.debug(ex) - raise exceptions.ValidationException(str(ex)) \ No newline at end of file + raise exceptions.ValidationException(str(ex)) diff --git a/monasca/v2/common/schemas/transforms_request_body_schema.py b/monasca/v2/common/schemas/transforms_request_body_schema.py index a30ef4ab9..9c8a2f10f 100644 --- a/monasca/v2/common/schemas/transforms_request_body_schema.py +++ b/monasca/v2/common/schemas/transforms_request_body_schema.py @@ -22,7 +22,8 @@ LOG = log.getLogger(__name__) transform_schema = { Required('name'): Schema(All(Any(str, unicode), Length(max=64))), Required('description'): Schema(All(Any(str, unicode), Length(max=250))), - Required('specification'): Schema(All(Any(str, unicode), Length(max=64536))), + Required('specification'): + Schema(All(Any(str, unicode), Length(max=64536))), Optional('enabled'): bool } @@ -34,4 +35,4 @@ def validate(msg): request_body_schema(msg) except Exception as ex: LOG.debug(ex) - raise exceptions.ValidationException(str(ex)) \ No newline at end of file + raise exceptions.ValidationException(str(ex)) diff --git a/monasca/v2/common/utils.py b/monasca/v2/common/utils.py index 5c15724cd..089240dc8 100644 --- a/monasca/v2/common/utils.py +++ b/monasca/v2/common/utils.py @@ -12,5 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. + def date_handler(obj): - return obj.isoformat() if hasattr(obj, 'isoformat') else obj \ No newline at end of file + return obj.isoformat() if hasattr(obj, 'isoformat') else obj diff --git a/monasca/v2/reference/alarm_definitions.py b/monasca/v2/reference/alarm_definitions.py new file mode 100644 index 000000000..185b3b6eb --- /dev/null +++ b/monasca/v2/reference/alarm_definitions.py @@ -0,0 +1,289 @@ +# Copyright 2014 Hewlett-Packard +# +# 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 json + +from pyparsing import ParseException +import falcon +from oslo.config import cfg + +from monasca.common.repositories import exceptions +from monasca.common import resource_api +from monasca.api.alarm_definitions_api_v2 import AlarmDefinitionsV2API +from monasca.expression_parser.alarm_expr_parser import AlarmExprParser +from monasca.openstack.common import log +from monasca.v2.reference import helpers +from monasca.v2.common.schemas import \ + alarm_definition_request_body_schema as schema_alarms +from monasca.v2.common.schemas import exceptions as schemas_exceptions +from monasca.v2.reference.helpers import read_json_msg_body +from monasca.common.messaging import exceptions as message_queue_exceptions + + +LOG = log.getLogger(__name__) + + +class AlarmDefinitions(AlarmDefinitionsV2API): + def __init__(self, global_conf): + try: + super(AlarmDefinitions, self).__init__(global_conf) + + self._region = cfg.CONF.region + + self._default_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + self._delegate_authorized_roles = \ + cfg.CONF.security.delegate_authorized_roles + self._post_metrics_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + \ + cfg.CONF.security.agent_authorized_roles + + self._message_queue = \ + resource_api.init_driver('monasca.messaging', + cfg.CONF.messaging.driver, + (['events'])) + + self._alarm_definitions_repo = resource_api.init_driver( + 'monasca.repositories', + cfg.CONF.repositories.alarm_definitions_driver) + + except Exception as ex: + LOG.exception(ex) + raise exceptions.RepositoryException(ex) + + @resource_api.Restify('/v2.0/alarm-definitions', method='post') + def do_post_alarm_definitions(self, req, res): + helpers.validate_authorization(req, self._default_authorized_roles) + + alarm_definition = read_json_msg_body(req) + + self._validate_alarm_definition(alarm_definition) + + tenant_id = helpers.get_tenant_id(req) + name = get_query_alarm_definition_name(alarm_definition) + expression = get_query_alarm_definition_expression( + alarm_definition) + description = get_query_alarm_definition_description( + alarm_definition) + severity = get_query_alarm_definition_severity( + alarm_definition) + match_by = get_query_alarm_definition_match_by( + alarm_definition) + alarm_actions = get_query_alarm_definition_alarm_actions( + alarm_definition) + undetermined_actions = \ + get_query_alarm_definition_undetermined_actions( + alarm_definition) + ok_actions = get_query_ok_actions(alarm_definition) + + result = self._alarm_definition_create(tenant_id, name, expression, + description, severity, match_by, + alarm_actions, + undetermined_actions, + ok_actions) + + helpers.add_links_to_resource(result, req.uri) + res.body = json.dumps(result, ensure_ascii=False).encode('utf8') + res.status = falcon.HTTP_201 + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='get') + def do_get_alarm_definition(self, req, res, id): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='put') + def do_put_alarm_definitions(self, req, res, id): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions', method='get') + def do_get_alarm_definitions(self, req, res): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='patch') + def do_patch_alarm_definitions(self, req, res, id): + res.status = '501 Not Implemented' + + @resource_api.Restify('/v2.0/alarm-definitions/{id}', method='delete') + def do_delete_alarm_definitions(self, req, res, id): + res.status = '501 Not Implemented' + + def _validate_alarm_definition(self, alarm_definition): + + try: + schema_alarms.validate(alarm_definition) + except schemas_exceptions.ValidationException as ex: + LOG.debug(ex) + raise falcon.HTTPBadRequest('Bad reqeust', ex.message) + + def _alarm_definition_create(self, tenant_id, name, expression, + description, severity, match_by, + alarm_actions, undetermined_actions, + ok_actions): + try: + sub_expr_list = AlarmExprParser(expression).get_sub_expr_list() + + alarm_definition_id = \ + self._alarm_definitions_repo.create_alarm_definition( + tenant_id, name, expression, sub_expr_list, description, + severity, match_by, alarm_actions, undetermined_actions, + ok_actions) + + self._send_alarm_definition_created_event(tenant_id, + alarm_definition_id, + name, expression, + sub_expr_list, + description, match_by) + result = ( + {u'alarm_actions': alarm_actions, u'ok_actions': ok_actions, + u'description': description, u'match_by': match_by, + u'severity': severity.lower(), u'actions_enabled': u'true', + u'undetermined_actions': undetermined_actions, + u'expression': expression, u'id': alarm_definition_id, + u'name': name}) + + return result + + except ParseException as ex: + LOG.exception(ex) + title = "Invalid alarm expression".encode('utf8') + msg = "parser failed on expression '{}' at column {}".format( + expression.encode('utf8'), str(ex.column).encode('utf')) + raise falcon.HTTPBadRequest(title, msg) + except exceptions.RepositoryException as ex: + LOG.exception(ex) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message[1]) + + def _send_alarm_definition_created_event(self, tenant_id, + alarm_definition_id, name, + expression, sub_expr_list, + description, match_by): + + alarm_definition_created_event_msg = { + u'alarm-definition-created': {u'tenantId': tenant_id, + u'alarmDefinitionId': + alarm_definition_id, + u'alarmName': name, + u'alarmDescription': description, + u'alarmExpression': expression, + u'matchBy': match_by}} + + sub_expr_event_msg = {} + for sub_expr in sub_expr_list: + sub_expr_event_msg[sub_expr.id] = { + u'function': sub_expr.get_normalized_func()} + metric_definition = { + u'name': sub_expr.get_normalized_metric_name()} + sub_expr_event_msg[sub_expr.id][ + u'metricDefinition'] = metric_definition + dimensions = {} + for dimension in sub_expr.get_dimensions_as_list(): + parsed_dimension = dimension.split("=") + dimensions[parsed_dimension[0]] = parsed_dimension[1] + metric_definition[u'dimensions'] = dimensions + sub_expr_event_msg[sub_expr.id][ + u'operator'] = sub_expr.get_normalized_operator() + sub_expr_event_msg[sub_expr.id][ + u'threshold'] = sub_expr.get_threshold() + sub_expr_event_msg[sub_expr.id][u'period'] = sub_expr.get_period() + sub_expr_event_msg[sub_expr.id][ + u'periods'] = sub_expr.get_periods() + sub_expr_event_msg[sub_expr.id][ + u'expression'] = sub_expr.get_fmtd_sub_expr() + + alarm_definition_created_event_msg[u'alarm-definition-created'][ + u'alarmSubExpressions'] = sub_expr_event_msg + + self._send_event(alarm_definition_created_event_msg) + + def _send_event(self, event_msg): + try: + self._message_queue.send_message( + json.dumps(event_msg, ensure_ascii=False).encode('utf8')) + except message_queue_exceptions.MessageQueueException as ex: + LOG.exception(ex) + raise falcon.HTTPInternalServerError( + 'Message queue service unavailable'.encode('utf8'), + ex.message.encode('utf8')) + + +def get_query_alarm_definition_name(alarm_definition): + try: + if 'name' in alarm_definition: + name = alarm_definition['name'] + return name + else: + raise Exception("Missing name") + except Exception as ex: + LOG.debug(ex) + raise falcon.HTTPBadRequest('Bad request', ex.message) + + +def get_query_alarm_definition_expression(alarm_definition): + try: + if 'expression' in alarm_definition: + expression = alarm_definition['expression'] + return expression + else: + raise Exception("Missing expression") + except Exception as ex: + LOG.debug(ex) + raise falcon.HTTPBadRequest('Bad request', ex.message) + +def get_query_alarm_definition_description(alarm_definition): + if 'description' in alarm_definition: + return alarm_definition['description'] + else: + return '' + + +def get_query_alarm_definition_severity(alarm_definition): + if 'severity' in alarm_definition: + severity = alarm_definition['severity'] + severity = severity.decode('utf8').lower() + if severity not in ['low', 'medium', 'high', 'critical']: + raise falcon.HTTPBadRequest('Bad request, Invalid severity') + return severity + else: + return '' + + +def get_query_alarm_definition_match_by(alarm_definition): + if 'match_by' in alarm_definition: + match_by = alarm_definition['match_by'] + return match_by + else: + return [] + + +def get_query_alarm_definition_alarm_actions(alarm_definition): + if 'alarm_actions' in alarm_definition: + alarm_actions = alarm_definition['alarm_actions'] + return alarm_actions + else: + return [] + + +def get_query_alarm_definition_undetermined_actions(alarm_definition): + if 'undetermined_actions' in alarm_definition: + undetermined_actions = alarm_definition['undetermined_actions'] + return undetermined_actions + else: + return [] + + +def get_query_ok_actions(alarm_definition): + if 'ok_actions' in alarm_definition: + ok_actions = alarm_definition['ok_actions'] + return ok_actions + else: + return [] diff --git a/monasca/v2/reference/events.py b/monasca/v2/reference/events.py index a966dda4a..19c8996cc 100644 --- a/monasca/v2/reference/events.py +++ b/monasca/v2/reference/events.py @@ -4,7 +4,7 @@ # 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 +# 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 @@ -24,7 +24,8 @@ from monasca.common.messaging import exceptions as message_queue_exceptions from monasca.common.messaging.message_formats import events_transform_factory from monasca.v2.common import utils from monasca.v2.common.schemas import exceptions as schemas_exceptions -from monasca.v2.common.schemas import events_request_body_schema as schemas_event +from monasca.v2.common.schemas import \ + events_request_body_schema as schemas_event from monasca.v2.reference import helpers from stevedore import driver @@ -36,13 +37,19 @@ class Events(monasca_events_api_v2.EventsV2API): def __init__(self, global_conf): super(Events, self).__init__(global_conf) self._region = cfg.CONF.region - self._default_authorized_roles = cfg.CONF.security.default_authorized_roles - self._delegate_authorized_roles = cfg.CONF.security.delegate_authorized_roles - self._post_events_authorized_roles = cfg.CONF.security.default_authorized_roles + \ - cfg.CONF.security.agent_authorized_roles - self._event_transform = events_transform_factory.create_events_transform() - self._message_queue = resource_api.init_driver('monasca.messaging', - cfg.CONF.messaging.driver, ['raw-events']) + self._default_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + self._delegate_authorized_roles = \ + cfg.CONF.security.delegate_authorized_roles + self._post_events_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + \ + cfg.CONF.security.agent_authorized_roles + self._event_transform = \ + events_transform_factory.create_events_transform() + self._message_queue = \ + resource_api.init_driver('monasca.messaging', + cfg.CONF.messaging.driver, + ['raw-events']) def _validate_event(self, event): """Validates the event @@ -63,11 +70,13 @@ class Events(monasca_events_api_v2.EventsV2API): :raises: falcon.HTTPServiceUnavailable """ try: - str_msg = json.dumps(event, default=utils.date_handler) + str_msg = json.dumps(event, default=utils.date_handler, + ensure_ascii=False).encode('utf8') self._message_queue.send_message(str_msg) except message_queue_exceptions.MessageQueueException as ex: LOG.exception(ex) - raise falcon.HTTPInternalServerError('Service unavailable', ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) @resource_api.Restify('/v2.0/events/', method='post') def do_post_events(self, req, res): @@ -76,6 +85,7 @@ class Events(monasca_events_api_v2.EventsV2API): event = helpers.read_http_resource(req) self._validate_event(event) tenant_id = helpers.get_tenant_id(req) - transformed_event = self._event_transform(event, tenant_id, self._region) + transformed_event = self._event_transform(event, tenant_id, + self._region) self._send_event(transformed_event) - res.status = falcon.HTTP_204 \ No newline at end of file + res.status = falcon.HTTP_204 diff --git a/monasca/v2/reference/helpers.py b/monasca/v2/reference/helpers.py index 75fba2b47..f76825697 100644 --- a/monasca/v2/reference/helpers.py +++ b/monasca/v2/reference/helpers.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. import datetime +import json import falcon from falcon.util.uri import parse_query_string @@ -25,6 +26,23 @@ import simplejson LOG = log.getLogger(__name__) +def read_json_msg_body(req): + ''' + Read the json_msg from the http request body and return them as JSON. + :param req: HTTP request object. + :return: Returns the metrics as a JSON object. + :raises falcon.HTTPBadRequest: + ''' + try: + msg = req.stream.read() + json_msg = json.loads(msg) + return json_msg + except ValueError as ex: + LOG.debug(ex) + raise falcon.HTTPBadRequest('Bad request', + 'Request body is not valid JSON') + + def validate_json_content_type(req): if req.content_type not in ['application/json']: raise falcon.HTTPBadRequest('Bad request', 'Bad content type. Must be ' @@ -79,7 +97,7 @@ def get_tenant_id(req): return req.get_header('X-TENANT-ID') -def get_cross_tenant_or_tenant_id(req, delegate_authorized_roles): +def get_x_tenant_or_tenant_id(req, delegate_authorized_roles): """Evaluates whether the tenant ID or cross tenant ID should be returned. :param req: HTTP request object. @@ -95,16 +113,24 @@ def get_cross_tenant_or_tenant_id(req, delegate_authorized_roles): return get_tenant_id(req) -def get_query_name(req): - """Returns the query param "name" if supplied. - +def get_query_name(req, name_required=False): + ''' + Returns the query param "name" if supplied. :param req: HTTP request object. - """ - params = parse_query_string(req.query_string) - name = '' - if 'name' in params: - name = params['name'] - return name + ''' + try: + params = parse_query_string(req.query_string) + if 'name' in params: + name = params['name'] + return name + else: + if name_required: + raise Exception("Missing name") + else: + return '' + except Exception as ex: + LOG.debug(ex) + raise falcon.HTTPBadRequest('Bad request', ex.message) def get_query_dimensions(req): diff --git a/monasca/v2/reference/metrics.py b/monasca/v2/reference/metrics.py index c69378579..d59bfcfc0 100644 --- a/monasca/v2/reference/metrics.py +++ b/monasca/v2/reference/metrics.py @@ -27,7 +27,11 @@ from monasca.v2.common import utils from monasca.v2.common.schemas import exceptions as schemas_exceptions from monasca.v2.common.schemas import \ metrics_request_body_schema as schemas_metrics + +from monasca.common.repositories import exceptions + from monasca.v2.reference import helpers +from monasca.v2.reference.helpers import read_json_msg_body LOG = log.getLogger(__name__) @@ -35,21 +39,30 @@ LOG = log.getLogger(__name__) class Metrics(monasca_api_v2.V2API): def __init__(self, global_conf): - super(Metrics, self).__init__(global_conf) - self._region = cfg.CONF.region - self._default_authorized_roles = \ - cfg.CONF.security.default_authorized_roles - self._delegate_authorized_roles = \ - cfg.CONF.security.delegate_authorized_roles - self._post_metrics_authorized_roles = \ - cfg.CONF.security.default_authorized_roles + \ - cfg.CONF.security.agent_authorized_roles - self._metrics_transform = \ - metrics_transform_factory.create_metrics_transform() - self._message_queue = resource_api.init_driver('monasca.messaging', - cfg.CONF.messaging.driver, ['metrics']) - self._metrics_repo = resource_api.init_driver('monasca.repositories', - cfg.CONF.repositories.metrics_driver) + + try: + super(Metrics, self).__init__(global_conf) + self._region = cfg.CONF.region + self._default_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + self._delegate_authorized_roles = \ + cfg.CONF.security.delegate_authorized_roles + self._post_metrics_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + \ + cfg.CONF.security.agent_authorized_roles + self._metrics_transform = \ + metrics_transform_factory.create_metrics_transform() + self._message_queue = resource_api.init_driver( + 'monasca.messaging', + cfg.CONF.messaging.driver, + ['metrics']) + self._metrics_repo = resource_api.init_driver( + 'monasca.repositories', cfg.CONF.repositories.metrics_driver) + + except Exception as ex: + LOG.exception(ex) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) def _validate_metrics(self, metrics): """Validates the metrics @@ -101,8 +114,8 @@ class Metrics(monasca_api_v2.V2API): raise falcon.HTTPServiceUnavailable('Service unavailable', ex.message) - def _measurement_list(self, tenant_id, name, dimensions, - start_timestamp, end_timestamp): + def _measurement_list(self, tenant_id, name, dimensions, start_timestamp, + end_timestamp): try: return self._metrics_repo.measurement_list(tenant_id, name, dimensions, @@ -113,21 +126,19 @@ class Metrics(monasca_api_v2.V2API): raise falcon.HTTPServiceUnavailable('Service unavailable', ex.message) - def _metric_statistics(self, tenant_id, name, dimensions, - start_timestamp, end_timestamp, statistics, period): + def _metric_statistics(self, tenant_id, name, dimensions, start_timestamp, + end_timestamp, statistics, period): try: return self._metrics_repo.metrics_statistics(tenant_id, name, - dimensions, - start_timestamp, - end_timestamp, - statistics, - period) + dimensions, + start_timestamp, + end_timestamp, + statistics, period) except Exception as ex: LOG.exception(ex) raise falcon.HTTPServiceUnavailable('Service unavailable', ex.message) - @resource_api.Restify('/v2.0/metrics/', method='post') def do_post_metrics(self, req, res): helpers.validate_json_content_type(req) @@ -135,8 +146,9 @@ class Metrics(monasca_api_v2.V2API): self._post_metrics_authorized_roles) metrics = helpers.read_http_resource(req) self._validate_metrics(metrics) - tenant_id = helpers.get_cross_tenant_or_tenant_id(req, - self._delegate_authorized_roles) + tenant_id = \ + helpers.get_x_tenant_or_tenant_id(req, + self._delegate_authorized_roles) transformed_metrics = self._metrics_transform(metrics, tenant_id, self._region) self._send_metrics(transformed_metrics) @@ -182,7 +194,7 @@ class Metrics(monasca_api_v2.V2API): statistics = helpers.get_query_statistics(req) period = helpers.get_query_period(req) result = self._metric_statistics(tenant_id, name, dimensions, - start_timestamp, end_timestamp, - statistics, period) + start_timestamp, end_timestamp, + statistics, period) res.body = json.dumps(result, ensure_ascii=False).encode('utf8') - res.status = falcon.HTTP_200 \ No newline at end of file + res.status = falcon.HTTP_200 diff --git a/monasca/v2/reference/notifications.py b/monasca/v2/reference/notifications.py index dfbfcaa4b..88e2f65ad 100644 --- a/monasca/v2/reference/notifications.py +++ b/monasca/v2/reference/notifications.py @@ -4,7 +4,7 @@ # 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 +# 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 @@ -26,20 +26,21 @@ from monasca.api import monasca_notifications_api_v2 from monasca.common import resource_api from monasca.common.repositories import exceptions as repository_exceptions from monasca.v2.common.schemas import exceptions as schemas_exceptions -from monasca.v2.common.schemas import notifications_request_body_schema as schemas_notifications +from monasca.v2.common.schemas import \ + notifications_request_body_schema as schemas_notifications from monasca.v2.reference import helpers LOG = log.getLogger(__name__) class Notifications(monasca_notifications_api_v2.NotificationsV2API): - def __init__(self, global_conf): super(Notifications, self).__init__(global_conf) self._region = cfg.CONF.region - self._default_authorized_roles = cfg.CONF.security.default_authorized_roles - self._notifications_repo = resource_api.init_driver('monasca.repositories', - cfg.CONF.repositories.notifications_driver) + self._default_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + self._notifications_repo = resource_api.init_driver( + 'monasca.repositories', cfg.CONF.repositories.notifications_driver) def _validate_notification(self, notification): """Validates the notification @@ -64,20 +65,16 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API): notification_type = notification['type'].upper() address = notification['address'] if self._notifications_repo.exists(tenant_id, name): - raise falcon.HTTPConflict( - 'Conflict', ('Notification Method already exists: tenant_id=%s name=%s' % - (tenant_id, name)), code=409) - self._notifications_repo.create_notification( - id, - tenant_id, - name, - notification_type, - address) + raise falcon.HTTPConflict('Conflict', ( + 'Notification Method already exists: tenant_id=%s name=%s' % ( + tenant_id, name)), code=409) + self._notifications_repo.create_notification(id, tenant_id, name, + notification_type, + address) except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError( - 'Service unavailable', - ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) def _update_notification(self, id, tenant_id, notification): """Update the notification using the repository. @@ -89,31 +86,24 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API): name = notification['name'] notification_type = notification['type'].upper() address = notification['address'] - self._notifications_repo.update_notification( - id, - tenant_id, - name, - notification_type, - address) + self._notifications_repo.update_notification(id, tenant_id, name, + notification_type, + address) except repository_exceptions.DoesNotExistException: helpers.raise_not_found_exception('notification', id, tenant_id) except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError( - 'Service unavailable', - ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) def _create_notification_response(self, id, notification, uri): name = notification['name'] notification_type = notification['type'].upper() address = notification['address'] - response = { - 'id': id, - 'name': name, - 'type': notification_type, - 'address': address - } - return json.dumps(helpers.add_links_to_resource(response, uri)) + response = {'id': id, 'name': name, 'type': notification_type, + 'address': address} + return json.dumps(helpers.add_links_to_resource(response, uri), + ensure_ascii=False).encode('utf8') def _list_notifications(self, tenant_id, uri): """Lists all notifications for this tenant id. @@ -128,9 +118,8 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API): helpers.add_links_to_resource_list(notifications, uri)) except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError( - 'Service unavailable', - ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) def _list_notification(self, tenant_id, notification_id, uri): """Lists the notification by id. @@ -141,17 +130,16 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API): """ try: notifications = self._notifications_repo.list_notification( - tenant_id, - notification_id) + tenant_id, notification_id) return json.dumps( helpers.add_links_to_resource(notifications, uri)) except repository_exceptions.DoesNotExistException: - helpers.raise_not_found_exception('notification', notification_id, tenant_id) + helpers.raise_not_found_exception('notification', notification_id, + tenant_id) except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError( - 'Service unavailable', - ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) def _delete_notification(self, tenant_id, notification_id): """Deletes the notification using the repository. @@ -161,16 +149,15 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API): :raises: falcon.HTTPServiceUnavailable,falcon.HTTPError (404) """ try: - self._notifications_repo.delete_notification( - tenant_id, - notification_id) + self._notifications_repo.delete_notification(tenant_id, + notification_id) except repository_exceptions.DoesNotExistException: - helpers.raise_not_found_exception('notification', notification_id, tenant_id) + helpers.raise_not_found_exception('notification', notification_id, + tenant_id) except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError( - 'Service unavailable', - ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) @resource_api.Restify('/v2.0/notification-methods', method='post') def do_post_notification_methods(self, req, res): @@ -181,10 +168,8 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API): id = uuidutils.generate_uuid() tenant_id = helpers.get_tenant_id(req) self._create_notification(id, tenant_id, notification) - res.body = self._create_notification_response( - id, - notification, - req.uri) + res.body = self._create_notification_response(id, notification, + req.uri) res.status = falcon.HTTP_200 @resource_api.Restify('/v2.0/notification-methods', method='get') @@ -216,8 +201,6 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API): self._validate_notification(notification) tenant_id = helpers.get_tenant_id(req) self._update_notification(id, tenant_id, notification) - res.body = self._create_notification_response( - id, - notification, - req.uri) + res.body = self._create_notification_response(id, notification, + req.uri) res.status = falcon.HTTP_200 diff --git a/monasca/v2/reference/transforms.py b/monasca/v2/reference/transforms.py index bd6ee7e11..b15eac187 100644 --- a/monasca/v2/reference/transforms.py +++ b/monasca/v2/reference/transforms.py @@ -4,7 +4,7 @@ # 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 +# 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 @@ -12,7 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. -# TODO: Used simplejson to read the yaml as simplejson transforms to "str" not "unicode" +# TODO: Used simplejson to read the yaml as simplejson transforms to "str" +# not "unicode" import json import simplejson @@ -26,7 +27,8 @@ from monasca.api import monasca_transforms_api_v2 from monasca.common import resource_api from monasca.common.repositories import exceptions as repository_exceptions from monasca.v2.common.schemas import exceptions as schemas_exceptions -from monasca.v2.common.schemas import transforms_request_body_schema as schemas_transforms +from monasca.v2.common.schemas import \ + transforms_request_body_schema as schemas_transforms from monasca.v2.reference import helpers from stevedore import driver @@ -38,9 +40,10 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API): def __init__(self, global_conf): super(Transforms, self).__init__(global_conf) self._region = cfg.CONF.region - self._default_authorized_roles = cfg.CONF.security.default_authorized_roles - self._transforms_repo = resource_api.init_driver('monasca.repositories', - cfg.CONF.repositories.transforms_driver) + self._default_authorized_roles = \ + cfg.CONF.security.default_authorized_roles + self._transforms_repo = resource_api.init_driver( + 'monasca.repositories', cfg.CONF.repositories.transforms_driver) def _validate_transform(self, transform): """Validates the transform @@ -65,32 +68,31 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API): description = transform['description'] specification = transform['specification'] enabled = transform['enabled'] - self._transforms_repo.create_transforms(id, tenant_id, name, description, specification, enabled) + self._transforms_repo.create_transforms(id, tenant_id, name, + description, specification, + enabled) except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError('Service unavailable', ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) def _create_transform_response(self, id, transform): name = transform['name'] description = transform['description'] specification = transform['specification'] enabled = transform['enabled'] - response = { - 'id': id, - 'name': name, - 'description': description, - 'specification': specification, - 'enabled': enabled - } + response = {'id': id, 'name': name, 'description': description, + 'specification': specification, 'enabled': enabled} return json.dumps(response) def _list_transforms(self, tenant_id): try: - transforms = self._transforms_repo.list_transforms(tenant_id) + transforms = self._transforms_repo.list_transforms(tenant_id) return json.dumps(transforms) except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError('Service unavailable', ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) def _delete_transform(self, tenant_id, transform_id): try: @@ -99,7 +101,8 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API): raise falcon.HTTPNotFound() except repository_exceptions.RepositoryException as ex: LOG.error(ex) - raise falcon.HTTPInternalServerError('Service unavailable', ex.message) + raise falcon.HTTPInternalServerError('Service unavailable', + ex.message) @resource_api.Restify('/v2.0/events/transforms', method='post') def do_post_transforms(self, req, res): @@ -120,9 +123,10 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API): res.body = self._list_transforms(tenant_id) res.status = falcon.HTTP_200 - @resource_api.Restify('/v2.0/events/transforms/{transform_id}', method='delete') + @resource_api.Restify('/v2.0/events/transforms/{transform_id}', + method='delete') def do_delete_transforms(self, req, res, transform_id): helpers.validate_authorization(req, self._default_authorized_roles) tenant_id = helpers.get_tenant_id(req) self._delete_transform(tenant_id, transform_id) - res.status = falcon.HTTP_204 \ No newline at end of file + res.status = falcon.HTTP_204 diff --git a/setup.cfg b/setup.cfg index fbec8ee63..99d9f576a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,9 @@ monasca.metrics_dispatcher = kafka = monasca.dispatcher.kafka_dispatcher:KafkaDispatcher v2_reference = monasca.v2.reference.metrics:Metrics +monasca.alarm_definitions_dispatcher = + v2_reference = monasca.v2.reference.alarm_definitions:AlarmDefinitions + monasca.events_dispatcher = v2_reference = monasca.v2.reference.events:Events @@ -62,6 +65,7 @@ monasca.repositories = influxdb_metrics_repo = monasca.common.repositories.influxdb.metrics_repository:MetricsRepository fake_events_repo = monasca.common.repositories.fake.events_repository:EventsRepository mysql_transforms_repo = monasca.common.repositories.mysql.transforms_repository:TransformsRepository + mysql_alarm_definitions_repo = monasca.common.repositories.mysql.alarm_definitions_repository:AlarmDefinitionsRepository mysql_notifications_repo = monasca.common.repositories.mysql.notifications_repository:NotificationsRepository [pbr]