Add alarm definition create resource
Change-Id: I65e1c9f8697632e3c2004282bb624ed814828864
This commit is contained in:
parent
44eaa39649
commit
ba39188b2f
|
@ -39,6 +39,9 @@ events_driver = none
|
||||||
# The driver to use for the transforms repository
|
# The driver to use for the transforms repository
|
||||||
transforms_driver = mysql_transforms_repo
|
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
|
# The driver to use for the notifications repository
|
||||||
notifications_driver = mysql_notifications_repo
|
notifications_driver = mysql_notifications_repo
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
@ -46,30 +46,6 @@ class V2API(object):
|
||||||
def do_get_statistics(self, req, res):
|
def do_get_statistics(self, req, res):
|
||||||
res.status = '501 Not Implemented'
|
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')
|
@resource_api.Restify('/v2.0/alarms/{id}', method='put')
|
||||||
def do_put_alarms(self, req, res, id):
|
def do_put_alarms(self, req, res, id):
|
||||||
res.status = '501 Not Implemented'
|
res.status = '501 Not Implemented'
|
||||||
|
|
|
@ -32,6 +32,7 @@ class TransformsV2API(object):
|
||||||
def do_get_transforms(self, req, res):
|
def do_get_transforms(self, req, res):
|
||||||
res.status = '501 Not Implemented'
|
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):
|
def do_delete_transforms(self, req, res, transform_id):
|
||||||
res.status = '501 Not Implemented'
|
res.status = '501 Not Implemented'
|
||||||
|
|
|
@ -24,49 +24,56 @@ from paste.deploy import loadapp
|
||||||
from wsgiref import simple_server
|
from wsgiref import simple_server
|
||||||
|
|
||||||
METRICS_DISPATCHER_NAMESPACE = 'monasca.metrics_dispatcher'
|
METRICS_DISPATCHER_NAMESPACE = 'monasca.metrics_dispatcher'
|
||||||
|
ALARM_DEFINITIONS_DISPATCHER_NAMESPACE = 'monasca.alarm_definitions_dispatcher'
|
||||||
EVENTS_DISPATCHER_NAMESPACE = 'monasca.events_dispatcher'
|
EVENTS_DISPATCHER_NAMESPACE = 'monasca.events_dispatcher'
|
||||||
TRANSFORMS_DISPATCHER_NAMESPACE = 'monasca.transforms_dispatcher'
|
TRANSFORMS_DISPATCHER_NAMESPACE = 'monasca.transforms_dispatcher'
|
||||||
NOTIFICATIONS_DISPATCHER_NAMESPACE = 'monasca.notifications_dispatcher'
|
NOTIFICATIONS_DISPATCHER_NAMESPACE = 'monasca.notifications_dispatcher'
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
global_opts = [
|
global_opts = [cfg.StrOpt('region', help='Region that API is running in')]
|
||||||
cfg.StrOpt('region', help='Region that API is running in')
|
|
||||||
]
|
|
||||||
|
|
||||||
cfg.CONF.register_opts(global_opts)
|
cfg.CONF.register_opts(global_opts)
|
||||||
|
|
||||||
security_opts = [
|
security_opts = [cfg.ListOpt('default_authorized_roles', default=['admin'],
|
||||||
cfg.ListOpt('default_authorized_roles', default=['admin'],
|
help='Roles that are allowed full access to the '
|
||||||
help='Roles that are allowed full access to the API'),
|
'API'),
|
||||||
cfg.ListOpt('agent_authorized_roles', default=['agent'],
|
cfg.ListOpt('agent_authorized_roles', default=['agent'],
|
||||||
help='Roles that are only allowed to POST to the API'),
|
help='Roles that are only allowed to POST to '
|
||||||
|
'the API'),
|
||||||
cfg.ListOpt('delegate_authorized_roles', default=['admin'],
|
cfg.ListOpt('delegate_authorized_roles', default=['admin'],
|
||||||
help='Roles that are allowed to POST metrics on behalf of another tenant')
|
help='Roles that are allowed to POST metrics on '
|
||||||
]
|
'behalf of another tenant')]
|
||||||
|
|
||||||
security_group = cfg.OptGroup(name='security', title='security')
|
security_group = cfg.OptGroup(name='security', title='security')
|
||||||
cfg.CONF.register_group(security_group)
|
cfg.CONF.register_group(security_group)
|
||||||
cfg.CONF.register_opts(security_opts, security_group)
|
cfg.CONF.register_opts(security_opts, security_group)
|
||||||
|
|
||||||
messaging_opts = [
|
messaging_opts = [cfg.StrOpt('driver', default='kafka',
|
||||||
cfg.StrOpt('driver', default='kafka', help='The message queue driver to use'),
|
help='The message queue driver to use'),
|
||||||
cfg.StrOpt('metrics_message_format', default='reference',
|
cfg.StrOpt('metrics_message_format', default='reference',
|
||||||
help='The type of metrics message format to publish to the message queue'),
|
help='The type of metrics message format to '
|
||||||
|
'publish to the message queue'),
|
||||||
cfg.StrOpt('events_message_format', default='reference',
|
cfg.StrOpt('events_message_format', default='reference',
|
||||||
help='The type of events message format to publish to the message queue')
|
help='The type of events message format to '
|
||||||
]
|
'publish to the message queue')]
|
||||||
|
|
||||||
messaging_group = cfg.OptGroup(name='messaging', title='messaging')
|
messaging_group = cfg.OptGroup(name='messaging', title='messaging')
|
||||||
cfg.CONF.register_group(messaging_group)
|
cfg.CONF.register_group(messaging_group)
|
||||||
cfg.CONF.register_opts(messaging_opts, messaging_group)
|
cfg.CONF.register_opts(messaging_opts, messaging_group)
|
||||||
|
|
||||||
repositories_opts = [
|
repositories_opts = [
|
||||||
cfg.StrOpt('metrics_driver', default='influxdb_metrics_repo', help='The repository driver to use for metrics'),
|
cfg.StrOpt('metrics_driver', default='influxdb_metrics_repo',
|
||||||
cfg.StrOpt('events_driver', default='fake_events_repo', help='The repository driver to use for events'),
|
help='The repository driver to use for metrics'),
|
||||||
cfg.StrOpt('transforms_driver', default='mysql_transforms_repo', help='The repository driver to use for transforms'),
|
cfg.StrOpt('alarm_definitions_driver',
|
||||||
cfg.StrOpt('notifications_driver', default='mysql_notifications_repo', help='The repository driver to use for notifications')
|
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')
|
repositories_group = cfg.OptGroup(name='repositories', title='repositories')
|
||||||
cfg.CONF.register_group(repositories_group)
|
cfg.CONF.register_group(repositories_group)
|
||||||
|
@ -74,78 +81,58 @@ cfg.CONF.register_opts(repositories_opts, repositories_group)
|
||||||
|
|
||||||
dispatcher_opts = [
|
dispatcher_opts = [
|
||||||
cfg.StrOpt('driver', default='monasca.v2.reference.metrics:Metrics',
|
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')
|
dispatcher_group = cfg.OptGroup(name='dispatcher', title='dispatcher')
|
||||||
cfg.CONF.register_group(dispatcher_group)
|
cfg.CONF.register_group(dispatcher_group)
|
||||||
cfg.CONF.register_opts(dispatcher_opts, dispatcher_group)
|
cfg.CONF.register_opts(dispatcher_opts, dispatcher_group)
|
||||||
|
|
||||||
kafka_opts = [
|
kafka_opts = [cfg.StrOpt('uri', help='Address to kafka server. For example: '
|
||||||
cfg.StrOpt('uri',
|
|
||||||
help='Address to kafka server. For example: '
|
|
||||||
'uri=192.168.1.191:9092'),
|
'uri=192.168.1.191:9092'),
|
||||||
cfg.StrOpt('metrics_topic',
|
cfg.StrOpt('metrics_topic', default='metrics',
|
||||||
default='metrics',
|
|
||||||
help='The topic that metrics will be published too.'),
|
help='The topic that metrics will be published too.'),
|
||||||
cfg.StrOpt('events_topic',
|
cfg.StrOpt('events_topic', default='raw-events',
|
||||||
default='raw-events',
|
|
||||||
help='The topic that events will be published too.'),
|
help='The topic that events will be published too.'),
|
||||||
cfg.StrOpt('group',
|
cfg.StrOpt('group', default='api',
|
||||||
default='api',
|
|
||||||
help='The group name that this service belongs to.'),
|
help='The group name that this service belongs to.'),
|
||||||
cfg.IntOpt('wait_time',
|
cfg.IntOpt('wait_time', default=1,
|
||||||
default=1,
|
help='The wait time when no messages on kafka '
|
||||||
help='The wait time when no messages on kafka queue.'),
|
'queue.'),
|
||||||
cfg.IntOpt('ack_time',
|
cfg.IntOpt('ack_time', default=20,
|
||||||
default=20,
|
|
||||||
help='The ack time back to kafka.'),
|
help='The ack time back to kafka.'),
|
||||||
cfg.IntOpt('max_retry',
|
cfg.IntOpt('max_retry', default=3,
|
||||||
default=3,
|
help='The number of retry when there is a '
|
||||||
help='The number of retry when there is a connection error.'),
|
'connection error.'),
|
||||||
cfg.BoolOpt('auto_commit',
|
cfg.BoolOpt('auto_commit', default=False,
|
||||||
default=False,
|
help='If automatically commmit when consume '
|
||||||
help='If automatically commmit when consume messages.'),
|
'messages.'),
|
||||||
cfg.BoolOpt('async',
|
cfg.BoolOpt('async', default=True, help='The type of posting.'),
|
||||||
default=True,
|
cfg.BoolOpt('compact', default=True, help=(
|
||||||
help='The type of posting.'),
|
'Specify if the message received should be parsed.'
|
||||||
cfg.BoolOpt('compact',
|
|
||||||
default=True,
|
|
||||||
help=('Specify if the message received should be parsed.'
|
|
||||||
'If True, message will not be parsed, otherwise '
|
'If True, message will not be parsed, otherwise '
|
||||||
'messages will be parsed.')),
|
'messages will be parsed.')),
|
||||||
cfg.MultiOpt('partitions',
|
cfg.MultiOpt('partitions', item_type=types.Integer(),
|
||||||
item_type=types.Integer(),
|
|
||||||
default=[0],
|
default=[0],
|
||||||
help='The sleep time when no messages on kafka queue.'),
|
help='The sleep time when no messages on kafka '
|
||||||
cfg.BoolOpt('drop_data',
|
'queue.'),
|
||||||
default=False,
|
cfg.BoolOpt('drop_data', default=False, help=(
|
||||||
help=('Specify if received data should be simply dropped. '
|
'Specify if received data should be simply dropped. '
|
||||||
'This parameter is only for testing purposes.')),
|
'This parameter is only for testing purposes.')), ]
|
||||||
]
|
|
||||||
|
|
||||||
kafka_group = cfg.OptGroup(name='kafka', title='title')
|
kafka_group = cfg.OptGroup(name='kafka', title='title')
|
||||||
cfg.CONF.register_group(kafka_group)
|
cfg.CONF.register_group(kafka_group)
|
||||||
cfg.CONF.register_opts(kafka_opts, kafka_group)
|
cfg.CONF.register_opts(kafka_opts, kafka_group)
|
||||||
|
|
||||||
influxdb_opts = [
|
influxdb_opts = [cfg.StrOpt('database_name'), cfg.StrOpt('ip_address'),
|
||||||
cfg.StrOpt('database_name'),
|
cfg.StrOpt('port'), cfg.StrOpt('user'),
|
||||||
cfg.StrOpt('ip_address'),
|
cfg.StrOpt('password')]
|
||||||
cfg.StrOpt('port'),
|
|
||||||
cfg.StrOpt('user'),
|
|
||||||
cfg.StrOpt('password')
|
|
||||||
]
|
|
||||||
|
|
||||||
influxdb_group = cfg.OptGroup(name='influxdb', title='influxdb')
|
influxdb_group = cfg.OptGroup(name='influxdb', title='influxdb')
|
||||||
cfg.CONF.register_group(influxdb_group)
|
cfg.CONF.register_group(influxdb_group)
|
||||||
cfg.CONF.register_opts(influxdb_opts, influxdb_group)
|
cfg.CONF.register_opts(influxdb_opts, influxdb_group)
|
||||||
|
|
||||||
mysql_opts = [
|
mysql_opts = [cfg.StrOpt('database_name'), cfg.StrOpt('hostname'),
|
||||||
cfg.StrOpt('database_name'),
|
cfg.StrOpt('username'), cfg.StrOpt('password')]
|
||||||
cfg.StrOpt('hostname'),
|
|
||||||
cfg.StrOpt('username'),
|
|
||||||
cfg.StrOpt('password')
|
|
||||||
]
|
|
||||||
|
|
||||||
mysql_group = cfg.OptGroup(name='mysql', title='mysql')
|
mysql_group = cfg.OptGroup(name='mysql', title='mysql')
|
||||||
cfg.CONF.register_group(mysql_group)
|
cfg.CONF.register_group(mysql_group)
|
||||||
|
@ -179,6 +166,11 @@ def api_app(conf):
|
||||||
app.add_resource('notifications', NOTIFICATIONS_DISPATCHER_NAMESPACE,
|
app.add_resource('notifications', NOTIFICATIONS_DISPATCHER_NAMESPACE,
|
||||||
cfg.CONF.dispatcher.driver, [conf])
|
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
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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'))
|
|
@ -25,21 +25,18 @@ RESOURCE_METHOD_FLAG = 'fab05a04-b861-4651-bd0c-9cb3eb9a6088'
|
||||||
LOG = log.getLogger(__name__)
|
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.
|
"""Initialize the resource driver and returns it.
|
||||||
|
|
||||||
:param namespace: the resource namespace (in setup.cfg).
|
:param namespace: the resource namespace (in setup.cfg).
|
||||||
:param driver_name: the driver name (in monasca.conf)
|
:param driver_name: the driver name (in monasca.conf)
|
||||||
:param invoke_args: args to pass to the driver (a tuple)
|
: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(
|
mgr = driver.DriverManager(
|
||||||
namespace = namespace,
|
namespace = namespace,
|
||||||
name = driver_name,
|
name = driver_name,
|
||||||
invoke_on_load = True,
|
invoke_on_load = True,
|
||||||
invoke_args = invoke_args_tuple
|
invoke_args = drv_invoke_args
|
||||||
)
|
)
|
||||||
return mgr.driver
|
return mgr.driver
|
||||||
|
|
||||||
|
@ -135,7 +132,7 @@ class ResourceAPI(falcon.API):
|
||||||
LOG.debug(self._routes)
|
LOG.debug(self._routes)
|
||||||
|
|
||||||
def add_resource(self, resource_name, namespace, driver_name,
|
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.
|
"""Loads the resource driver, and adds it to the routes.
|
||||||
|
|
||||||
:param resource_name: the name of the resource.
|
:param resource_name: the name of the resource.
|
||||||
|
|
|
@ -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())
|
|
@ -12,6 +12,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
class MockAuthFilter(object):
|
class MockAuthFilter(object):
|
||||||
'''
|
'''
|
||||||
This authorization filter doesn't do any authentication, it just copies the
|
This authorization filter doesn't do any authentication, it just copies the
|
||||||
|
@ -27,6 +28,7 @@ class MockAuthFilter(object):
|
||||||
env['HTTP_X_ROLES'] = 'admin'
|
env['HTTP_X_ROLES'] = 'admin'
|
||||||
return self.app(env, start_response)
|
return self.app(env, start_response)
|
||||||
|
|
||||||
|
|
||||||
def filter_factory(global_conf, **local_conf):
|
def filter_factory(global_conf, **local_conf):
|
||||||
def validator_filter(app):
|
def validator_filter(app):
|
||||||
return MockAuthFilter(app, local_conf)
|
return MockAuthFilter(app, local_conf)
|
||||||
|
|
|
@ -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))
|
|
@ -19,8 +19,8 @@ from monasca.v2.common.schemas import exceptions
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
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)):
|
||||||
dimensions_schema = Schema({All(Any(str, unicode), Length(max=255)): All(Any(str, unicode), Length(max=255))})
|
All(Any(str, unicode), Length(max=255))})
|
||||||
|
|
||||||
|
|
||||||
def validate(dimensions):
|
def validate(dimensions):
|
||||||
|
|
|
@ -20,7 +20,9 @@ from monasca.v2.common.schemas import exceptions
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
# TODO: Add regex to validate key/values don't use any excluded characters.
|
# 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):
|
def validate(body):
|
||||||
|
|
|
@ -12,5 +12,6 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
class ValidationException(Exception):
|
class ValidationException(Exception):
|
||||||
pass
|
pass
|
|
@ -22,7 +22,8 @@ LOG = log.getLogger(__name__)
|
||||||
transform_schema = {
|
transform_schema = {
|
||||||
Required('name'): Schema(All(Any(str, unicode), Length(max=64))),
|
Required('name'): Schema(All(Any(str, unicode), Length(max=64))),
|
||||||
Required('description'): Schema(All(Any(str, unicode), Length(max=250))),
|
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
|
Optional('enabled'): bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,5 +12,6 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
def date_handler(obj):
|
def date_handler(obj):
|
||||||
return obj.isoformat() if hasattr(obj, 'isoformat') else obj
|
return obj.isoformat() if hasattr(obj, 'isoformat') else obj
|
|
@ -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 []
|
|
@ -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.common.messaging.message_formats import events_transform_factory
|
||||||
from monasca.v2.common import utils
|
from monasca.v2.common import utils
|
||||||
from monasca.v2.common.schemas import exceptions as schemas_exceptions
|
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 monasca.v2.reference import helpers
|
||||||
|
|
||||||
from stevedore import driver
|
from stevedore import driver
|
||||||
|
@ -36,13 +37,19 @@ class Events(monasca_events_api_v2.EventsV2API):
|
||||||
def __init__(self, global_conf):
|
def __init__(self, global_conf):
|
||||||
super(Events, self).__init__(global_conf)
|
super(Events, self).__init__(global_conf)
|
||||||
self._region = cfg.CONF.region
|
self._region = cfg.CONF.region
|
||||||
self._default_authorized_roles = cfg.CONF.security.default_authorized_roles
|
self._default_authorized_roles = \
|
||||||
self._delegate_authorized_roles = cfg.CONF.security.delegate_authorized_roles
|
cfg.CONF.security.default_authorized_roles
|
||||||
self._post_events_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
|
cfg.CONF.security.agent_authorized_roles
|
||||||
self._event_transform = events_transform_factory.create_events_transform()
|
self._event_transform = \
|
||||||
self._message_queue = resource_api.init_driver('monasca.messaging',
|
events_transform_factory.create_events_transform()
|
||||||
cfg.CONF.messaging.driver, ['raw-events'])
|
self._message_queue = \
|
||||||
|
resource_api.init_driver('monasca.messaging',
|
||||||
|
cfg.CONF.messaging.driver,
|
||||||
|
['raw-events'])
|
||||||
|
|
||||||
def _validate_event(self, event):
|
def _validate_event(self, event):
|
||||||
"""Validates the event
|
"""Validates the event
|
||||||
|
@ -63,11 +70,13 @@ class Events(monasca_events_api_v2.EventsV2API):
|
||||||
:raises: falcon.HTTPServiceUnavailable
|
:raises: falcon.HTTPServiceUnavailable
|
||||||
"""
|
"""
|
||||||
try:
|
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)
|
self._message_queue.send_message(str_msg)
|
||||||
except message_queue_exceptions.MessageQueueException as ex:
|
except message_queue_exceptions.MessageQueueException as ex:
|
||||||
LOG.exception(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')
|
@resource_api.Restify('/v2.0/events/', method='post')
|
||||||
def do_post_events(self, req, res):
|
def do_post_events(self, req, res):
|
||||||
|
@ -76,6 +85,7 @@ class Events(monasca_events_api_v2.EventsV2API):
|
||||||
event = helpers.read_http_resource(req)
|
event = helpers.read_http_resource(req)
|
||||||
self._validate_event(event)
|
self._validate_event(event)
|
||||||
tenant_id = helpers.get_tenant_id(req)
|
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)
|
self._send_event(transformed_event)
|
||||||
res.status = falcon.HTTP_204
|
res.status = falcon.HTTP_204
|
|
@ -12,6 +12,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from falcon.util.uri import parse_query_string
|
from falcon.util.uri import parse_query_string
|
||||||
|
@ -25,6 +26,23 @@ import simplejson
|
||||||
LOG = log.getLogger(__name__)
|
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):
|
def validate_json_content_type(req):
|
||||||
if req.content_type not in ['application/json']:
|
if req.content_type not in ['application/json']:
|
||||||
raise falcon.HTTPBadRequest('Bad request', 'Bad content type. Must be '
|
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')
|
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.
|
"""Evaluates whether the tenant ID or cross tenant ID should be returned.
|
||||||
|
|
||||||
:param req: HTTP request object.
|
: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)
|
return get_tenant_id(req)
|
||||||
|
|
||||||
|
|
||||||
def get_query_name(req):
|
def get_query_name(req, name_required=False):
|
||||||
"""Returns the query param "name" if supplied.
|
'''
|
||||||
|
Returns the query param "name" if supplied.
|
||||||
:param req: HTTP request object.
|
:param req: HTTP request object.
|
||||||
"""
|
'''
|
||||||
|
try:
|
||||||
params = parse_query_string(req.query_string)
|
params = parse_query_string(req.query_string)
|
||||||
name = ''
|
|
||||||
if 'name' in params:
|
if 'name' in params:
|
||||||
name = params['name']
|
name = params['name']
|
||||||
return 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):
|
def get_query_dimensions(req):
|
||||||
|
|
|
@ -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 exceptions as schemas_exceptions
|
||||||
from monasca.v2.common.schemas import \
|
from monasca.v2.common.schemas import \
|
||||||
metrics_request_body_schema as schemas_metrics
|
metrics_request_body_schema as schemas_metrics
|
||||||
|
|
||||||
|
from monasca.common.repositories import exceptions
|
||||||
|
|
||||||
from monasca.v2.reference import helpers
|
from monasca.v2.reference import helpers
|
||||||
|
from monasca.v2.reference.helpers import read_json_msg_body
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
@ -35,6 +39,8 @@ LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
class Metrics(monasca_api_v2.V2API):
|
class Metrics(monasca_api_v2.V2API):
|
||||||
def __init__(self, global_conf):
|
def __init__(self, global_conf):
|
||||||
|
|
||||||
|
try:
|
||||||
super(Metrics, self).__init__(global_conf)
|
super(Metrics, self).__init__(global_conf)
|
||||||
self._region = cfg.CONF.region
|
self._region = cfg.CONF.region
|
||||||
self._default_authorized_roles = \
|
self._default_authorized_roles = \
|
||||||
|
@ -46,10 +52,17 @@ class Metrics(monasca_api_v2.V2API):
|
||||||
cfg.CONF.security.agent_authorized_roles
|
cfg.CONF.security.agent_authorized_roles
|
||||||
self._metrics_transform = \
|
self._metrics_transform = \
|
||||||
metrics_transform_factory.create_metrics_transform()
|
metrics_transform_factory.create_metrics_transform()
|
||||||
self._message_queue = resource_api.init_driver('monasca.messaging',
|
self._message_queue = resource_api.init_driver(
|
||||||
cfg.CONF.messaging.driver, ['metrics'])
|
'monasca.messaging',
|
||||||
self._metrics_repo = resource_api.init_driver('monasca.repositories',
|
cfg.CONF.messaging.driver,
|
||||||
cfg.CONF.repositories.metrics_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):
|
def _validate_metrics(self, metrics):
|
||||||
"""Validates the metrics
|
"""Validates the metrics
|
||||||
|
@ -101,8 +114,8 @@ class Metrics(monasca_api_v2.V2API):
|
||||||
raise falcon.HTTPServiceUnavailable('Service unavailable',
|
raise falcon.HTTPServiceUnavailable('Service unavailable',
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
def _measurement_list(self, tenant_id, name, dimensions,
|
def _measurement_list(self, tenant_id, name, dimensions, start_timestamp,
|
||||||
start_timestamp, end_timestamp):
|
end_timestamp):
|
||||||
try:
|
try:
|
||||||
return self._metrics_repo.measurement_list(tenant_id, name,
|
return self._metrics_repo.measurement_list(tenant_id, name,
|
||||||
dimensions,
|
dimensions,
|
||||||
|
@ -113,21 +126,19 @@ class Metrics(monasca_api_v2.V2API):
|
||||||
raise falcon.HTTPServiceUnavailable('Service unavailable',
|
raise falcon.HTTPServiceUnavailable('Service unavailable',
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
def _metric_statistics(self, tenant_id, name, dimensions,
|
def _metric_statistics(self, tenant_id, name, dimensions, start_timestamp,
|
||||||
start_timestamp, end_timestamp, statistics, period):
|
end_timestamp, statistics, period):
|
||||||
try:
|
try:
|
||||||
return self._metrics_repo.metrics_statistics(tenant_id, name,
|
return self._metrics_repo.metrics_statistics(tenant_id, name,
|
||||||
dimensions,
|
dimensions,
|
||||||
start_timestamp,
|
start_timestamp,
|
||||||
end_timestamp,
|
end_timestamp,
|
||||||
statistics,
|
statistics, period)
|
||||||
period)
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
raise falcon.HTTPServiceUnavailable('Service unavailable',
|
raise falcon.HTTPServiceUnavailable('Service unavailable',
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
|
|
||||||
@resource_api.Restify('/v2.0/metrics/', method='post')
|
@resource_api.Restify('/v2.0/metrics/', method='post')
|
||||||
def do_post_metrics(self, req, res):
|
def do_post_metrics(self, req, res):
|
||||||
helpers.validate_json_content_type(req)
|
helpers.validate_json_content_type(req)
|
||||||
|
@ -135,7 +146,8 @@ class Metrics(monasca_api_v2.V2API):
|
||||||
self._post_metrics_authorized_roles)
|
self._post_metrics_authorized_roles)
|
||||||
metrics = helpers.read_http_resource(req)
|
metrics = helpers.read_http_resource(req)
|
||||||
self._validate_metrics(metrics)
|
self._validate_metrics(metrics)
|
||||||
tenant_id = helpers.get_cross_tenant_or_tenant_id(req,
|
tenant_id = \
|
||||||
|
helpers.get_x_tenant_or_tenant_id(req,
|
||||||
self._delegate_authorized_roles)
|
self._delegate_authorized_roles)
|
||||||
transformed_metrics = self._metrics_transform(metrics, tenant_id,
|
transformed_metrics = self._metrics_transform(metrics, tenant_id,
|
||||||
self._region)
|
self._region)
|
||||||
|
|
|
@ -26,20 +26,21 @@ from monasca.api import monasca_notifications_api_v2
|
||||||
from monasca.common import resource_api
|
from monasca.common import resource_api
|
||||||
from monasca.common.repositories import exceptions as repository_exceptions
|
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 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
|
from monasca.v2.reference import helpers
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
|
|
||||||
def __init__(self, global_conf):
|
def __init__(self, global_conf):
|
||||||
super(Notifications, self).__init__(global_conf)
|
super(Notifications, self).__init__(global_conf)
|
||||||
self._region = cfg.CONF.region
|
self._region = cfg.CONF.region
|
||||||
self._default_authorized_roles = cfg.CONF.security.default_authorized_roles
|
self._default_authorized_roles = \
|
||||||
self._notifications_repo = resource_api.init_driver('monasca.repositories',
|
cfg.CONF.security.default_authorized_roles
|
||||||
cfg.CONF.repositories.notifications_driver)
|
self._notifications_repo = resource_api.init_driver(
|
||||||
|
'monasca.repositories', cfg.CONF.repositories.notifications_driver)
|
||||||
|
|
||||||
def _validate_notification(self, notification):
|
def _validate_notification(self, notification):
|
||||||
"""Validates the notification
|
"""Validates the notification
|
||||||
|
@ -64,19 +65,15 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
notification_type = notification['type'].upper()
|
notification_type = notification['type'].upper()
|
||||||
address = notification['address']
|
address = notification['address']
|
||||||
if self._notifications_repo.exists(tenant_id, name):
|
if self._notifications_repo.exists(tenant_id, name):
|
||||||
raise falcon.HTTPConflict(
|
raise falcon.HTTPConflict('Conflict', (
|
||||||
'Conflict', ('Notification Method already exists: tenant_id=%s name=%s' %
|
'Notification Method already exists: tenant_id=%s name=%s' % (
|
||||||
(tenant_id, name)), code=409)
|
tenant_id, name)), code=409)
|
||||||
self._notifications_repo.create_notification(
|
self._notifications_repo.create_notification(id, tenant_id, name,
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
name,
|
|
||||||
notification_type,
|
notification_type,
|
||||||
address)
|
address)
|
||||||
except repository_exceptions.RepositoryException as ex:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(ex)
|
LOG.error(ex)
|
||||||
raise falcon.HTTPInternalServerError(
|
raise falcon.HTTPInternalServerError('Service unavailable',
|
||||||
'Service unavailable',
|
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
def _update_notification(self, id, tenant_id, notification):
|
def _update_notification(self, id, tenant_id, notification):
|
||||||
|
@ -89,31 +86,24 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
name = notification['name']
|
name = notification['name']
|
||||||
notification_type = notification['type'].upper()
|
notification_type = notification['type'].upper()
|
||||||
address = notification['address']
|
address = notification['address']
|
||||||
self._notifications_repo.update_notification(
|
self._notifications_repo.update_notification(id, tenant_id, name,
|
||||||
id,
|
|
||||||
tenant_id,
|
|
||||||
name,
|
|
||||||
notification_type,
|
notification_type,
|
||||||
address)
|
address)
|
||||||
except repository_exceptions.DoesNotExistException:
|
except repository_exceptions.DoesNotExistException:
|
||||||
helpers.raise_not_found_exception('notification', id, tenant_id)
|
helpers.raise_not_found_exception('notification', id, tenant_id)
|
||||||
except repository_exceptions.RepositoryException as ex:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(ex)
|
LOG.error(ex)
|
||||||
raise falcon.HTTPInternalServerError(
|
raise falcon.HTTPInternalServerError('Service unavailable',
|
||||||
'Service unavailable',
|
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
def _create_notification_response(self, id, notification, uri):
|
def _create_notification_response(self, id, notification, uri):
|
||||||
name = notification['name']
|
name = notification['name']
|
||||||
notification_type = notification['type'].upper()
|
notification_type = notification['type'].upper()
|
||||||
address = notification['address']
|
address = notification['address']
|
||||||
response = {
|
response = {'id': id, 'name': name, 'type': notification_type,
|
||||||
'id': id,
|
'address': address}
|
||||||
'name': name,
|
return json.dumps(helpers.add_links_to_resource(response, uri),
|
||||||
'type': notification_type,
|
ensure_ascii=False).encode('utf8')
|
||||||
'address': address
|
|
||||||
}
|
|
||||||
return json.dumps(helpers.add_links_to_resource(response, uri))
|
|
||||||
|
|
||||||
def _list_notifications(self, tenant_id, uri):
|
def _list_notifications(self, tenant_id, uri):
|
||||||
"""Lists all notifications for this tenant id.
|
"""Lists all notifications for this tenant id.
|
||||||
|
@ -128,8 +118,7 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
helpers.add_links_to_resource_list(notifications, uri))
|
helpers.add_links_to_resource_list(notifications, uri))
|
||||||
except repository_exceptions.RepositoryException as ex:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(ex)
|
LOG.error(ex)
|
||||||
raise falcon.HTTPInternalServerError(
|
raise falcon.HTTPInternalServerError('Service unavailable',
|
||||||
'Service unavailable',
|
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
def _list_notification(self, tenant_id, notification_id, uri):
|
def _list_notification(self, tenant_id, notification_id, uri):
|
||||||
|
@ -141,16 +130,15 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
notifications = self._notifications_repo.list_notification(
|
notifications = self._notifications_repo.list_notification(
|
||||||
tenant_id,
|
tenant_id, notification_id)
|
||||||
notification_id)
|
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
helpers.add_links_to_resource(notifications, uri))
|
helpers.add_links_to_resource(notifications, uri))
|
||||||
except repository_exceptions.DoesNotExistException:
|
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:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(ex)
|
LOG.error(ex)
|
||||||
raise falcon.HTTPInternalServerError(
|
raise falcon.HTTPInternalServerError('Service unavailable',
|
||||||
'Service unavailable',
|
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
def _delete_notification(self, tenant_id, notification_id):
|
def _delete_notification(self, tenant_id, notification_id):
|
||||||
|
@ -161,15 +149,14 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
:raises: falcon.HTTPServiceUnavailable,falcon.HTTPError (404)
|
:raises: falcon.HTTPServiceUnavailable,falcon.HTTPError (404)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self._notifications_repo.delete_notification(
|
self._notifications_repo.delete_notification(tenant_id,
|
||||||
tenant_id,
|
|
||||||
notification_id)
|
notification_id)
|
||||||
except repository_exceptions.DoesNotExistException:
|
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:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(ex)
|
LOG.error(ex)
|
||||||
raise falcon.HTTPInternalServerError(
|
raise falcon.HTTPInternalServerError('Service unavailable',
|
||||||
'Service unavailable',
|
|
||||||
ex.message)
|
ex.message)
|
||||||
|
|
||||||
@resource_api.Restify('/v2.0/notification-methods', method='post')
|
@resource_api.Restify('/v2.0/notification-methods', method='post')
|
||||||
|
@ -181,9 +168,7 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
id = uuidutils.generate_uuid()
|
id = uuidutils.generate_uuid()
|
||||||
tenant_id = helpers.get_tenant_id(req)
|
tenant_id = helpers.get_tenant_id(req)
|
||||||
self._create_notification(id, tenant_id, notification)
|
self._create_notification(id, tenant_id, notification)
|
||||||
res.body = self._create_notification_response(
|
res.body = self._create_notification_response(id, notification,
|
||||||
id,
|
|
||||||
notification,
|
|
||||||
req.uri)
|
req.uri)
|
||||||
res.status = falcon.HTTP_200
|
res.status = falcon.HTTP_200
|
||||||
|
|
||||||
|
@ -216,8 +201,6 @@ class Notifications(monasca_notifications_api_v2.NotificationsV2API):
|
||||||
self._validate_notification(notification)
|
self._validate_notification(notification)
|
||||||
tenant_id = helpers.get_tenant_id(req)
|
tenant_id = helpers.get_tenant_id(req)
|
||||||
self._update_notification(id, tenant_id, notification)
|
self._update_notification(id, tenant_id, notification)
|
||||||
res.body = self._create_notification_response(
|
res.body = self._create_notification_response(id, notification,
|
||||||
id,
|
|
||||||
notification,
|
|
||||||
req.uri)
|
req.uri)
|
||||||
res.status = falcon.HTTP_200
|
res.status = falcon.HTTP_200
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# 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 json
|
||||||
import simplejson
|
import simplejson
|
||||||
|
@ -26,7 +27,8 @@ from monasca.api import monasca_transforms_api_v2
|
||||||
from monasca.common import resource_api
|
from monasca.common import resource_api
|
||||||
from monasca.common.repositories import exceptions as repository_exceptions
|
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 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 monasca.v2.reference import helpers
|
||||||
|
|
||||||
from stevedore import driver
|
from stevedore import driver
|
||||||
|
@ -38,9 +40,10 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API):
|
||||||
def __init__(self, global_conf):
|
def __init__(self, global_conf):
|
||||||
super(Transforms, self).__init__(global_conf)
|
super(Transforms, self).__init__(global_conf)
|
||||||
self._region = cfg.CONF.region
|
self._region = cfg.CONF.region
|
||||||
self._default_authorized_roles = cfg.CONF.security.default_authorized_roles
|
self._default_authorized_roles = \
|
||||||
self._transforms_repo = resource_api.init_driver('monasca.repositories',
|
cfg.CONF.security.default_authorized_roles
|
||||||
cfg.CONF.repositories.transforms_driver)
|
self._transforms_repo = resource_api.init_driver(
|
||||||
|
'monasca.repositories', cfg.CONF.repositories.transforms_driver)
|
||||||
|
|
||||||
def _validate_transform(self, transform):
|
def _validate_transform(self, transform):
|
||||||
"""Validates the transform
|
"""Validates the transform
|
||||||
|
@ -65,23 +68,21 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API):
|
||||||
description = transform['description']
|
description = transform['description']
|
||||||
specification = transform['specification']
|
specification = transform['specification']
|
||||||
enabled = transform['enabled']
|
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:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(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):
|
def _create_transform_response(self, id, transform):
|
||||||
name = transform['name']
|
name = transform['name']
|
||||||
description = transform['description']
|
description = transform['description']
|
||||||
specification = transform['specification']
|
specification = transform['specification']
|
||||||
enabled = transform['enabled']
|
enabled = transform['enabled']
|
||||||
response = {
|
response = {'id': id, 'name': name, 'description': description,
|
||||||
'id': id,
|
'specification': specification, 'enabled': enabled}
|
||||||
'name': name,
|
|
||||||
'description': description,
|
|
||||||
'specification': specification,
|
|
||||||
'enabled': enabled
|
|
||||||
}
|
|
||||||
return json.dumps(response)
|
return json.dumps(response)
|
||||||
|
|
||||||
def _list_transforms(self, tenant_id):
|
def _list_transforms(self, tenant_id):
|
||||||
|
@ -90,7 +91,8 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API):
|
||||||
return json.dumps(transforms)
|
return json.dumps(transforms)
|
||||||
except repository_exceptions.RepositoryException as ex:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(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):
|
def _delete_transform(self, tenant_id, transform_id):
|
||||||
try:
|
try:
|
||||||
|
@ -99,7 +101,8 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API):
|
||||||
raise falcon.HTTPNotFound()
|
raise falcon.HTTPNotFound()
|
||||||
except repository_exceptions.RepositoryException as ex:
|
except repository_exceptions.RepositoryException as ex:
|
||||||
LOG.error(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')
|
@resource_api.Restify('/v2.0/events/transforms', method='post')
|
||||||
def do_post_transforms(self, req, res):
|
def do_post_transforms(self, req, res):
|
||||||
|
@ -120,7 +123,8 @@ class Transforms(monasca_transforms_api_v2.TransformsV2API):
|
||||||
res.body = self._list_transforms(tenant_id)
|
res.body = self._list_transforms(tenant_id)
|
||||||
res.status = falcon.HTTP_200
|
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):
|
def do_delete_transforms(self, req, res, transform_id):
|
||||||
helpers.validate_authorization(req, self._default_authorized_roles)
|
helpers.validate_authorization(req, self._default_authorized_roles)
|
||||||
tenant_id = helpers.get_tenant_id(req)
|
tenant_id = helpers.get_tenant_id(req)
|
||||||
|
|
|
@ -37,6 +37,9 @@ monasca.metrics_dispatcher =
|
||||||
kafka = monasca.dispatcher.kafka_dispatcher:KafkaDispatcher
|
kafka = monasca.dispatcher.kafka_dispatcher:KafkaDispatcher
|
||||||
v2_reference = monasca.v2.reference.metrics:Metrics
|
v2_reference = monasca.v2.reference.metrics:Metrics
|
||||||
|
|
||||||
|
monasca.alarm_definitions_dispatcher =
|
||||||
|
v2_reference = monasca.v2.reference.alarm_definitions:AlarmDefinitions
|
||||||
|
|
||||||
monasca.events_dispatcher =
|
monasca.events_dispatcher =
|
||||||
v2_reference = monasca.v2.reference.events:Events
|
v2_reference = monasca.v2.reference.events:Events
|
||||||
|
|
||||||
|
@ -62,6 +65,7 @@ monasca.repositories =
|
||||||
influxdb_metrics_repo = monasca.common.repositories.influxdb.metrics_repository:MetricsRepository
|
influxdb_metrics_repo = monasca.common.repositories.influxdb.metrics_repository:MetricsRepository
|
||||||
fake_events_repo = monasca.common.repositories.fake.events_repository:EventsRepository
|
fake_events_repo = monasca.common.repositories.fake.events_repository:EventsRepository
|
||||||
mysql_transforms_repo = monasca.common.repositories.mysql.transforms_repository:TransformsRepository
|
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
|
mysql_notifications_repo = monasca.common.repositories.mysql.notifications_repository:NotificationsRepository
|
||||||
|
|
||||||
[pbr]
|
[pbr]
|
||||||
|
|
Loading…
Reference in New Issue