Prometheus datasource
The datasource currently supports notifications from Prometheus. There are a few related tasks that will be handled in future commits: 1. Prometheus may send the ip of the instance instead of its name. We need to find a way (in the transformer)to identify the correct instance in the graph. 2. The 'vitrage event post' API should be changed so it can handle events from Prometheus. 3. Need to implement get_all in the datasource Change-Id: Id1cba3e060b70c232891a34bf6793c185a074a7b Depends-On: Ide6906ee477aa7df9ab0918d3b45a7001afdcf74 Implements: blueprint prometheus-datasource
This commit is contained in:
parent
feb76c1eb1
commit
2382591f9f
39
vitrage/datasources/prometheus/__init__.py
Normal file
39
vitrage/datasources/prometheus/__init__.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Copyright 2018 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_config import cfg
|
||||||
|
from vitrage.common.constants import DatasourceOpts as DSOpts
|
||||||
|
from vitrage.common.constants import UpdateMethod
|
||||||
|
|
||||||
|
PROMETHEUS_DATASOURCE = 'prometheus'
|
||||||
|
|
||||||
|
OPTS = [
|
||||||
|
cfg.StrOpt(DSOpts.TRANSFORMER,
|
||||||
|
default='vitrage.datasources.prometheus.transformer.'
|
||||||
|
'PrometheusTransformer',
|
||||||
|
help='Prometheus transformer class path',
|
||||||
|
required=True),
|
||||||
|
cfg.StrOpt(DSOpts.DRIVER,
|
||||||
|
default='vitrage.datasources.prometheus.driver.'
|
||||||
|
'PrometheusDriver',
|
||||||
|
help='Prometheus driver class path',
|
||||||
|
required=True),
|
||||||
|
cfg.StrOpt(DSOpts.UPDATE_METHOD,
|
||||||
|
default=UpdateMethod.PUSH,
|
||||||
|
help='None: updates only via Vitrage periodic snapshots.'
|
||||||
|
'Pull: updates every [changes_interval] seconds.'
|
||||||
|
'Push: updates by getting notifications from the'
|
||||||
|
' datasource itself.',
|
||||||
|
required=True),
|
||||||
|
]
|
151
vitrage/datasources/prometheus/driver.py
Normal file
151
vitrage/datasources/prometheus/driver.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Copyright 2018 - Nokia
|
||||||
|
#
|
||||||
|
# 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 collections import namedtuple
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from vitrage.common.constants import DatasourceAction
|
||||||
|
from vitrage.common.constants import DatasourceProperties as DSProps
|
||||||
|
from vitrage.datasources.alarm_driver_base import AlarmDriverBase
|
||||||
|
from vitrage.datasources.prometheus import PROMETHEUS_DATASOURCE
|
||||||
|
from vitrage.datasources.prometheus.properties import get_alarm_update_time
|
||||||
|
from vitrage.datasources.prometheus.properties import get_label
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusAlertStatus \
|
||||||
|
as PAlertStatus
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusLabels \
|
||||||
|
as PLabels
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusProperties \
|
||||||
|
as PProps
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
PROMETHEUS_EVENT_TYPE = 'prometheus.alarm'
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusDriver(AlarmDriverBase):
|
||||||
|
AlarmKey = namedtuple('AlarmKey', ['alert_name', 'instance'])
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
super(PrometheusDriver, self).__init__()
|
||||||
|
self.conf = conf
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
def _vitrage_type(self):
|
||||||
|
return PROMETHEUS_DATASOURCE
|
||||||
|
|
||||||
|
def _alarm_key(self, alarm):
|
||||||
|
return self.AlarmKey(alert_name=get_label(alarm, PLabels.ALERT_NAME),
|
||||||
|
instance=str(get_label(alarm, PLabels.INSTANCE)))
|
||||||
|
|
||||||
|
def _is_erroneous(self, alarm):
|
||||||
|
return alarm and PAlertStatus.FIRING == alarm.get(PProps.STATUS)
|
||||||
|
|
||||||
|
def _is_valid(self, alarm):
|
||||||
|
if not alarm or PProps.STATUS not in alarm:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _status_changed(self, new_alarm, old_alarm):
|
||||||
|
return new_alarm.get(PProps.STATUS) != old_alarm.get(PProps.STATUS)
|
||||||
|
|
||||||
|
def _get_alarms(self):
|
||||||
|
# TODO(iafek): should be implemented
|
||||||
|
return []
|
||||||
|
|
||||||
|
def enrich_event(self, event, event_type):
|
||||||
|
"""Get an event from Prometheus and create a list of alarm events
|
||||||
|
|
||||||
|
:param event: dictionary of this form:
|
||||||
|
{
|
||||||
|
"status": "firing",
|
||||||
|
"groupLabels": {
|
||||||
|
"alertname": "HighInodeUsage"
|
||||||
|
},
|
||||||
|
"groupKey": "{}:{alertname=\"HighInodeUsage\"}",
|
||||||
|
"commonAnnotations": {
|
||||||
|
"mount_point": "/%",
|
||||||
|
"description": "\"Consider ssh\"ing into the instance \"\n",
|
||||||
|
"title": "High number of inode usage",
|
||||||
|
"value": "96.81%",
|
||||||
|
"device": "/dev/vda1%",
|
||||||
|
"runbook": "troubleshooting/filesystem_alerts_inodes.md"
|
||||||
|
},
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"status": "firing",
|
||||||
|
"labels": {
|
||||||
|
"severity": "critical",
|
||||||
|
"fstype": "ext4",
|
||||||
|
"instance": "localhost:9100",
|
||||||
|
"job": "node",
|
||||||
|
"alertname": "HighInodeUsage",
|
||||||
|
"device": "/dev/vda1",
|
||||||
|
"mountpoint": "/"
|
||||||
|
},
|
||||||
|
"endsAt": "0001-01-01T00:00:00Z",
|
||||||
|
"generatorURL": "http://devstack-rocky-4:9090/graph?g0.htm1",
|
||||||
|
"startsAt": "2018-05-03T12:25:38.231388525Z",
|
||||||
|
"annotations": {
|
||||||
|
"mount_point": "/%",
|
||||||
|
"description": "\"Consider ssh\"ing into the instance\"\n",
|
||||||
|
"title": "High number of inode usage",
|
||||||
|
"value": "96.81%",
|
||||||
|
"device": "/dev/vda1%",
|
||||||
|
"runbook": "troubleshooting/filesystem_alerts_inodes.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": "4",
|
||||||
|
"receiver": "vitrage",
|
||||||
|
"externalURL": "http://devstack-rocky-4:9093",
|
||||||
|
"commonLabels": {
|
||||||
|
"severity": "critical",
|
||||||
|
"fstype": "ext4",
|
||||||
|
"instance": "localhost:9100",
|
||||||
|
"job": "node",
|
||||||
|
"alertname": "HighInodeUsage",
|
||||||
|
"device": "/dev/vda1",
|
||||||
|
"mountpoint": "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:param event_type: The type of the event. Always 'prometheus.alarm'.
|
||||||
|
:return: a list of events, one per Prometheus alert
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOG.debug('Going to enrich event: %s', str(event))
|
||||||
|
|
||||||
|
alarms = []
|
||||||
|
|
||||||
|
for alarm in event.get(PProps.ALERTS, []):
|
||||||
|
alarm[DSProps.EVENT_TYPE] = event_type
|
||||||
|
alarm[PProps.STATUS] = event[PProps.STATUS]
|
||||||
|
|
||||||
|
old_alarm = self._old_alarm(alarm)
|
||||||
|
alarm = self._filter_and_cache_alarm(alarm, old_alarm,
|
||||||
|
self._filter_get_erroneous,
|
||||||
|
get_alarm_update_time(alarm))
|
||||||
|
|
||||||
|
if alarm:
|
||||||
|
alarms.append(alarm)
|
||||||
|
|
||||||
|
LOG.debug('Enriched event. Created alarm events: %s', str(alarms))
|
||||||
|
|
||||||
|
return self.make_pickleable(alarms, PROMETHEUS_DATASOURCE,
|
||||||
|
DatasourceAction.UPDATE)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_event_types():
|
||||||
|
return [PROMETHEUS_EVENT_TYPE]
|
56
vitrage/datasources/prometheus/properties.py
Normal file
56
vitrage/datasources/prometheus/properties.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Copyright 2018 - Nokia
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusProperties(object):
|
||||||
|
STATUS = 'status'
|
||||||
|
ALERTS = 'alerts'
|
||||||
|
ANNOTATIONS = 'annotations'
|
||||||
|
LABELS = 'labels'
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAlertStatus(object):
|
||||||
|
FIRING = 'firing'
|
||||||
|
RESOLVED = 'resolved'
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAlertProperties(object):
|
||||||
|
STARTS_AT = 'startsAt'
|
||||||
|
ENDS_AT = 'endsAt'
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusAnnotations(object):
|
||||||
|
TITLE = 'title' # A human friendly name of the alert
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusLabels(object):
|
||||||
|
SEVERITY = 'severity'
|
||||||
|
INSTANCE = 'instance'
|
||||||
|
ALERT_NAME = 'alertname' # A (unique?) name of the alert
|
||||||
|
|
||||||
|
|
||||||
|
def get_alarm_update_time(alarm):
|
||||||
|
return alarm.get(PrometheusAlertProperties.ENDS_AT) if \
|
||||||
|
PrometheusAlertProperties.ENDS_AT in alarm else \
|
||||||
|
alarm.get(PrometheusAlertProperties.STARTS_AT)
|
||||||
|
|
||||||
|
|
||||||
|
def get_annotation(alarm, annotation):
|
||||||
|
annotations = alarm.get(PrometheusProperties.ANNOTATIONS)
|
||||||
|
return annotations.get(annotation) if annotations else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_label(alarm, label):
|
||||||
|
labels = alarm.get(PrometheusProperties.LABELS)
|
||||||
|
return labels.get(label) if labels else None
|
93
vitrage/datasources/prometheus/transformer.py
Normal file
93
vitrage/datasources/prometheus/transformer.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# Copyright 2018 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_log import log as logging
|
||||||
|
|
||||||
|
from vitrage.common.constants import DatasourceProperties as DSProps
|
||||||
|
from vitrage.common.constants import EdgeLabel
|
||||||
|
from vitrage.common.constants import EntityCategory as ECategory
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage.datasources.alarm_transformer_base import AlarmTransformerBase
|
||||||
|
from vitrage.datasources.prometheus import PROMETHEUS_DATASOURCE
|
||||||
|
from vitrage.datasources.prometheus.properties import get_alarm_update_time
|
||||||
|
from vitrage.datasources.prometheus.properties import get_label
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusAlertStatus \
|
||||||
|
as PAlertStatus
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusLabels \
|
||||||
|
as PLabels
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusProperties \
|
||||||
|
as PProps
|
||||||
|
from vitrage.datasources import transformer_base as tbase
|
||||||
|
import vitrage.graph.utils as graph_utils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusTransformer(AlarmTransformerBase):
|
||||||
|
|
||||||
|
def __init__(self, transformers, conf):
|
||||||
|
super(PrometheusTransformer, self).__init__(transformers, conf)
|
||||||
|
|
||||||
|
def _create_snapshot_entity_vertex(self, entity_event):
|
||||||
|
# TODO(iafek): should be implemented
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _create_update_entity_vertex(self, entity_event):
|
||||||
|
metadata = {
|
||||||
|
VProps.NAME: get_label(entity_event, PLabels.ALERT_NAME),
|
||||||
|
VProps.SEVERITY: get_label(entity_event, PLabels.SEVERITY),
|
||||||
|
PProps.STATUS: entity_event.get(PProps.STATUS),
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph_utils.create_vertex(
|
||||||
|
self._create_entity_key(entity_event),
|
||||||
|
vitrage_category=ECategory.ALARM,
|
||||||
|
vitrage_type=entity_event[DSProps.ENTITY_TYPE],
|
||||||
|
vitrage_sample_timestamp=entity_event[DSProps.SAMPLE_DATE],
|
||||||
|
entity_state=self._get_alarm_state(entity_event),
|
||||||
|
update_timestamp=get_alarm_update_time(entity_event),
|
||||||
|
metadata=metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_update_neighbors(self, entity_event):
|
||||||
|
graph_neighbors = entity_event.get(self.QUERY_RESULT, [])
|
||||||
|
|
||||||
|
return [self._create_neighbor(entity_event,
|
||||||
|
graph_neighbor[VProps.ID],
|
||||||
|
graph_neighbor[VProps.VITRAGE_TYPE],
|
||||||
|
EdgeLabel.ON,
|
||||||
|
neighbor_category=ECategory.RESOURCE)
|
||||||
|
for graph_neighbor in graph_neighbors]
|
||||||
|
|
||||||
|
def _create_entity_key(self, entity_event):
|
||||||
|
return tbase.build_key((ECategory.ALARM,
|
||||||
|
entity_event[DSProps.ENTITY_TYPE],
|
||||||
|
get_label(entity_event, PLabels.ALERT_NAME),
|
||||||
|
get_label(entity_event, PLabels.INSTANCE)))
|
||||||
|
|
||||||
|
def get_vitrage_type(self):
|
||||||
|
return PROMETHEUS_DATASOURCE
|
||||||
|
|
||||||
|
def _ok_status(self, entity_event):
|
||||||
|
return entity_event and \
|
||||||
|
PAlertStatus.RESOLVED == entity_event.get(PProps.STATUS)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_enrich_query(event):
|
||||||
|
LOG.debug('event for enrich query: %s', str(event))
|
||||||
|
hostname = get_label(event, PLabels.INSTANCE)
|
||||||
|
if not hostname:
|
||||||
|
return None
|
||||||
|
hostname = hostname[:hostname.index(':')]
|
||||||
|
return {VProps.ID: hostname}
|
@ -571,6 +571,27 @@ def simple_aodh_alarm_notification_generators(alarm_num,
|
|||||||
return tg.get_trace_generators(test_entity_spec_list)
|
return tg.get_trace_generators(test_entity_spec_list)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_prometheus_alarm_generators(update_vals=None):
|
||||||
|
"""A function for returning Prometheus alarm event generators.
|
||||||
|
|
||||||
|
Returns generators for a given number of Prometheus alarms.
|
||||||
|
|
||||||
|
:param update_vals: preset values for ALL update events
|
||||||
|
:return: generators for alarms as specified
|
||||||
|
"""
|
||||||
|
|
||||||
|
test_entity_spec_list = [({
|
||||||
|
tg.DYNAMIC_INFO_FKEY: tg.DRIVER_PROMETHEUS_UPDATE_D,
|
||||||
|
tg.STATIC_INFO_FKEY: None,
|
||||||
|
tg.EXTERNAL_INFO_KEY: update_vals,
|
||||||
|
tg.MAPPING_KEY: None,
|
||||||
|
tg.NAME_KEY: 'Prometheus alarm generator',
|
||||||
|
tg.NUM_EVENTS: 1
|
||||||
|
})]
|
||||||
|
|
||||||
|
return tg.get_trace_generators(test_entity_spec_list)
|
||||||
|
|
||||||
|
|
||||||
def simple_k8s_nodes_generators(nodes_num, snapshot_events=0):
|
def simple_k8s_nodes_generators(nodes_num, snapshot_events=0):
|
||||||
mapping = ['vm-{0}'.format(index) for index in range(nodes_num)]
|
mapping = ['vm-{0}'.format(index) for index in range(nodes_num)]
|
||||||
|
|
||||||
|
@ -198,18 +198,8 @@ def simple_doctor_alarm_generators(update_vals=None):
|
|||||||
:param update_vals: preset values for ALL update events
|
:param update_vals: preset values for ALL update events
|
||||||
:return: generators for alarms as specified
|
:return: generators for alarms as specified
|
||||||
"""
|
"""
|
||||||
|
return _simple_alarm_generators('Doctor',
|
||||||
test_entity_spec_list = [({
|
tg.TRANS_DOCTOR_UPDATE_D, update_vals)
|
||||||
tg.DYNAMIC_INFO_FKEY: tg.TRANS_DOCTOR_UPDATE_D,
|
|
||||||
tg.DYNAMIC_INFO_FPATH: tg.MOCK_TRANSFORMER_PATH,
|
|
||||||
tg.STATIC_INFO_FKEY: None,
|
|
||||||
tg.EXTERNAL_INFO_KEY: update_vals,
|
|
||||||
tg.MAPPING_KEY: None,
|
|
||||||
tg.NAME_KEY: 'Doctor alarm generator',
|
|
||||||
tg.NUM_EVENTS: 1
|
|
||||||
})]
|
|
||||||
|
|
||||||
return tg.get_trace_generators(test_entity_spec_list)
|
|
||||||
|
|
||||||
|
|
||||||
def simple_collectd_alarm_generators(update_vals=None):
|
def simple_collectd_alarm_generators(update_vals=None):
|
||||||
@ -217,17 +207,41 @@ def simple_collectd_alarm_generators(update_vals=None):
|
|||||||
|
|
||||||
Returns generators for a given number of Collectd alarms.
|
Returns generators for a given number of Collectd alarms.
|
||||||
|
|
||||||
|
:param update_vals: preset values for ALL update events
|
||||||
|
:return: generators for alarms as specified
|
||||||
|
"""
|
||||||
|
return _simple_alarm_generators('Collectd',
|
||||||
|
tg.TRANS_COLLECTD_UPDATE_D, update_vals)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_prometheus_alarm_generators(update_vals=None):
|
||||||
|
"""A function for returning Prometheus alarm event generators.
|
||||||
|
|
||||||
|
Returns generators for a given number of Prometheus alarms.
|
||||||
|
|
||||||
|
:param update_vals: preset values for ALL update events
|
||||||
|
:return: generators for alarms as specified
|
||||||
|
"""
|
||||||
|
return _simple_alarm_generators('Prometheus',
|
||||||
|
tg.TRANS_PROMETHEUS_UPDATE_D, update_vals)
|
||||||
|
|
||||||
|
|
||||||
|
def _simple_alarm_generators(datasource, sample_file, update_vals):
|
||||||
|
"""A function for returning alarm event generators.
|
||||||
|
|
||||||
|
Returns generators for a given number of alarms.
|
||||||
|
|
||||||
:param update_vals: preset values for ALL update events
|
:param update_vals: preset values for ALL update events
|
||||||
:return: generators for alarms as specified
|
:return: generators for alarms as specified
|
||||||
"""
|
"""
|
||||||
|
|
||||||
test_entity_spec_list = [({
|
test_entity_spec_list = [({
|
||||||
tg.DYNAMIC_INFO_FKEY: tg.TRANS_COLLECTD_UPDATE_D,
|
tg.DYNAMIC_INFO_FKEY: sample_file,
|
||||||
tg.DYNAMIC_INFO_FPATH: tg.MOCK_TRANSFORMER_PATH,
|
tg.DYNAMIC_INFO_FPATH: tg.MOCK_TRANSFORMER_PATH,
|
||||||
tg.STATIC_INFO_FKEY: None,
|
tg.STATIC_INFO_FKEY: None,
|
||||||
tg.EXTERNAL_INFO_KEY: update_vals,
|
tg.EXTERNAL_INFO_KEY: update_vals,
|
||||||
tg.MAPPING_KEY: None,
|
tg.MAPPING_KEY: None,
|
||||||
tg.NAME_KEY: 'Collectd alarm generator',
|
tg.NAME_KEY: datasource + ' alarm generator',
|
||||||
tg.NUM_EVENTS: 1
|
tg.NUM_EVENTS: 1
|
||||||
})]
|
})]
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ DRIVER_INST_SNAPSHOT_S = 'driver_inst_snapshot_static.json'
|
|||||||
DRIVER_INST_UPDATE_D = 'driver_inst_update_dynamic.json'
|
DRIVER_INST_UPDATE_D = 'driver_inst_update_dynamic.json'
|
||||||
DRIVER_NAGIOS_SNAPSHOT_D = 'driver_nagios_snapshot_dynamic.json'
|
DRIVER_NAGIOS_SNAPSHOT_D = 'driver_nagios_snapshot_dynamic.json'
|
||||||
DRIVER_NAGIOS_SNAPSHOT_S = 'driver_nagios_snapshot_static.json'
|
DRIVER_NAGIOS_SNAPSHOT_S = 'driver_nagios_snapshot_static.json'
|
||||||
|
DRIVER_PROMETHEUS_UPDATE_D = 'driver_prometheus_update_dynamic.json'
|
||||||
DRIVER_ZABBIX_SNAPSHOT_D = 'driver_zabbix_snapshot_dynamic.json'
|
DRIVER_ZABBIX_SNAPSHOT_D = 'driver_zabbix_snapshot_dynamic.json'
|
||||||
DRIVER_SWITCH_SNAPSHOT_D = 'driver_switch_snapshot_dynamic.json'
|
DRIVER_SWITCH_SNAPSHOT_D = 'driver_switch_snapshot_dynamic.json'
|
||||||
DRIVER_STATIC_SNAPSHOT_D = 'driver_static_snapshot_dynamic.json'
|
DRIVER_STATIC_SNAPSHOT_D = 'driver_static_snapshot_dynamic.json'
|
||||||
@ -76,6 +77,7 @@ TRANS_AODH_SNAPSHOT_D = 'transformer_aodh_snapshot_dynamic.json'
|
|||||||
TRANS_AODH_UPDATE_D = 'transformer_aodh_update_dynamic.json'
|
TRANS_AODH_UPDATE_D = 'transformer_aodh_update_dynamic.json'
|
||||||
TRANS_DOCTOR_UPDATE_D = 'transformer_doctor_update_dynamic.json'
|
TRANS_DOCTOR_UPDATE_D = 'transformer_doctor_update_dynamic.json'
|
||||||
TRANS_COLLECTD_UPDATE_D = 'transformer_collectd_update_dynamic.json'
|
TRANS_COLLECTD_UPDATE_D = 'transformer_collectd_update_dynamic.json'
|
||||||
|
TRANS_PROMETHEUS_UPDATE_D = 'transformer_prometheus_update_dynamic.json'
|
||||||
TRANS_INST_SNAPSHOT_D = 'transformer_inst_snapshot_dynamic.json'
|
TRANS_INST_SNAPSHOT_D = 'transformer_inst_snapshot_dynamic.json'
|
||||||
TRANS_INST_SNAPSHOT_S = 'transformer_inst_snapshot_static.json'
|
TRANS_INST_SNAPSHOT_S = 'transformer_inst_snapshot_static.json'
|
||||||
TRANS_HOST_SNAPSHOT_D = 'transformer_host_snapshot_dynamic.json'
|
TRANS_HOST_SNAPSHOT_D = 'transformer_host_snapshot_dynamic.json'
|
||||||
@ -118,8 +120,8 @@ class EventTraceGenerator(object):
|
|||||||
|
|
||||||
static_info_parsers = \
|
static_info_parsers = \
|
||||||
{DRIVER_AODH_UPDATE_D: _get_aodh_alarm_update_driver_values,
|
{DRIVER_AODH_UPDATE_D: _get_aodh_alarm_update_driver_values,
|
||||||
DRIVER_DOCTOR_UPDATE_D: _get_doctor_update_driver_values,
|
DRIVER_DOCTOR_UPDATE_D: _get_simple_update_driver_values,
|
||||||
DRIVER_COLLECTD_UPDATE_D: _get_collectd_update_driver_values,
|
DRIVER_COLLECTD_UPDATE_D: _get_simple_update_driver_values,
|
||||||
DRIVER_KUBE_SNAPSHOT_D: _get_k8s_node_snapshot_driver_values,
|
DRIVER_KUBE_SNAPSHOT_D: _get_k8s_node_snapshot_driver_values,
|
||||||
DRIVER_INST_SNAPSHOT_D: _get_vm_snapshot_driver_values,
|
DRIVER_INST_SNAPSHOT_D: _get_vm_snapshot_driver_values,
|
||||||
DRIVER_INST_UPDATE_D: _get_vm_update_driver_values,
|
DRIVER_INST_UPDATE_D: _get_vm_update_driver_values,
|
||||||
@ -135,11 +137,13 @@ class EventTraceGenerator(object):
|
|||||||
DRIVER_ZABBIX_SNAPSHOT_D: _get_zabbix_alarm_driver_values,
|
DRIVER_ZABBIX_SNAPSHOT_D: _get_zabbix_alarm_driver_values,
|
||||||
DRIVER_CONSISTENCY_UPDATE_D:
|
DRIVER_CONSISTENCY_UPDATE_D:
|
||||||
_get_consistency_update_driver_values,
|
_get_consistency_update_driver_values,
|
||||||
|
DRIVER_PROMETHEUS_UPDATE_D: _get_simple_update_driver_values,
|
||||||
|
|
||||||
TRANS_AODH_SNAPSHOT_D: _get_trans_aodh_alarm_snapshot_values,
|
TRANS_AODH_SNAPSHOT_D: _get_trans_aodh_alarm_snapshot_values,
|
||||||
TRANS_AODH_UPDATE_D: _get_trans_aodh_alarm_snapshot_values,
|
TRANS_AODH_UPDATE_D: _get_trans_aodh_alarm_snapshot_values,
|
||||||
TRANS_DOCTOR_UPDATE_D: _get_trans_doctor_alarm_update_values,
|
TRANS_DOCTOR_UPDATE_D: _get_simple_trans_alarm_update_values,
|
||||||
TRANS_COLLECTD_UPDATE_D: _get_trans_collectd_alarm_update_values,
|
TRANS_COLLECTD_UPDATE_D: _get_simple_trans_alarm_update_values,
|
||||||
|
TRANS_PROMETHEUS_UPDATE_D: _get_simple_trans_alarm_update_values,
|
||||||
TRANS_INST_SNAPSHOT_D: _get_trans_vm_snapshot_values,
|
TRANS_INST_SNAPSHOT_D: _get_trans_vm_snapshot_values,
|
||||||
TRANS_HOST_SNAPSHOT_D: _get_trans_host_snapshot_values,
|
TRANS_HOST_SNAPSHOT_D: _get_trans_host_snapshot_values,
|
||||||
TRANS_ZONE_SNAPSHOT_D: _get_trans_zone_snapshot_values}
|
TRANS_ZONE_SNAPSHOT_D: _get_trans_zone_snapshot_values}
|
||||||
@ -277,7 +281,7 @@ def _get_k8s_node_snapshot_driver_values(spec):
|
|||||||
return static_values
|
return static_values
|
||||||
|
|
||||||
|
|
||||||
def _get_doctor_update_driver_values(spec):
|
def _get_simple_update_driver_values(spec):
|
||||||
"""Generates the static driver values for Doctor monitor notification.
|
"""Generates the static driver values for Doctor monitor notification.
|
||||||
|
|
||||||
:param spec: specification of event generation.
|
:param spec: specification of event generation.
|
||||||
@ -288,17 +292,6 @@ def _get_doctor_update_driver_values(spec):
|
|||||||
return [combine_data(None, None, spec.get(EXTERNAL_INFO_KEY, None))]
|
return [combine_data(None, None, spec.get(EXTERNAL_INFO_KEY, None))]
|
||||||
|
|
||||||
|
|
||||||
def _get_collectd_update_driver_values(spec):
|
|
||||||
"""Generates the static driver values for Collectd monitor notification.
|
|
||||||
|
|
||||||
:param spec: specification of event generation.
|
|
||||||
:type spec: dict
|
|
||||||
:return: list of notifications of Doctor monitor
|
|
||||||
:rtype: list
|
|
||||||
"""
|
|
||||||
return [combine_data(None, None, spec.get(EXTERNAL_INFO_KEY, None))]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_zone_snapshot_driver_values(spec):
|
def _get_zone_snapshot_driver_values(spec):
|
||||||
"""Generates the static driver values for each zone.
|
"""Generates the static driver values for each zone.
|
||||||
|
|
||||||
@ -768,12 +761,12 @@ def _get_aodh_alarm_update_driver_values(spec):
|
|||||||
return static_values
|
return static_values
|
||||||
|
|
||||||
|
|
||||||
def _get_trans_doctor_alarm_update_values(spec):
|
def _get_simple_trans_alarm_update_values(spec):
|
||||||
"""Generates the dynamic transformer values for a Doctor alarm
|
"""Generates the dynamic transformer values for a simple alarm
|
||||||
|
|
||||||
:param spec: specification of event generation.
|
:param spec: specification of event generation.
|
||||||
:type spec: dict
|
:type spec: dict
|
||||||
:return: list of dynamic transformer values for a Doctor alarm
|
:return: list of dynamic transformer values for a simple alarm
|
||||||
:rtype: list with one alarm
|
:rtype: list with one alarm
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -785,23 +778,6 @@ def _get_trans_doctor_alarm_update_values(spec):
|
|||||||
None, spec.get(EXTERNAL_INFO_KEY, None))]
|
None, spec.get(EXTERNAL_INFO_KEY, None))]
|
||||||
|
|
||||||
|
|
||||||
def _get_trans_collectd_alarm_update_values(spec):
|
|
||||||
"""Generates the dynamic transformer values for a Collectd alarm
|
|
||||||
|
|
||||||
:param spec: specification of event generation.
|
|
||||||
:type spec: dict
|
|
||||||
:return: list of dynamic transformer values for a Collectd alarm
|
|
||||||
:rtype: list with one alarm
|
|
||||||
"""
|
|
||||||
|
|
||||||
static_info = None
|
|
||||||
if spec[STATIC_INFO_FKEY] is not None:
|
|
||||||
static_info = utils.load_specs(spec[STATIC_INFO_FKEY])
|
|
||||||
|
|
||||||
return [combine_data(static_info,
|
|
||||||
None, spec.get(EXTERNAL_INFO_KEY, None))]
|
|
||||||
|
|
||||||
|
|
||||||
def combine_data(static_info, mapping_info, external_info):
|
def combine_data(static_info, mapping_info, external_info):
|
||||||
if external_info:
|
if external_info:
|
||||||
mapping_info = utils.merge_vals(mapping_info, external_info)
|
mapping_info = utils.merge_vals(mapping_info, external_info)
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"status": "firing",
|
||||||
|
"groupLabels": {
|
||||||
|
"alertname": "HighInodeUsage"
|
||||||
|
},
|
||||||
|
"groupKey": "{}:{alertname=\"HighInodeUsage\"}",
|
||||||
|
"commonAnnotations": {
|
||||||
|
"mount_point": "/%",
|
||||||
|
"description": "\"Consider ssh\"ing into the instance and removing files or clean\ntemp files\"\n",
|
||||||
|
"title": "High number of inode usage",
|
||||||
|
"value": "96.81%",
|
||||||
|
"device": "/dev/vda1%",
|
||||||
|
"runbook": "troubleshooting/filesystem_alerts_inodes.md"
|
||||||
|
},
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"status": "firing",
|
||||||
|
"labels": {
|
||||||
|
"severity": "critical",
|
||||||
|
"fstype": "ext4",
|
||||||
|
"instance": "localhost:9100",
|
||||||
|
"job": "node",
|
||||||
|
"alertname": "HighInodeUsage",
|
||||||
|
"device": "/dev/vda1",
|
||||||
|
"mountpoint": "/"
|
||||||
|
},
|
||||||
|
"endsAt": "0001-01-01T00:00:00Z",
|
||||||
|
"generatorURL": "http://devstack-rocky-4:9090/graph?g0.expr=node_filesystem_files_free%7Bfstype%3D~%22%28ext.%7Cxfs%29%22%2Cjob%3D%22node%22%7D+%2F+node_filesystem_files%7Bfstype%3D~%22%28ext.%7Cxfs%29%22%2Cjob%3D%22node%22%7D+%2A+100+%3C%3D+100&g0.tab=1",
|
||||||
|
"startsAt": "2018-05-03T12:25:38.231388525Z",
|
||||||
|
"annotations": {
|
||||||
|
"mount_point": "/%",
|
||||||
|
"description": "\"Consider ssh\"ing into the instance and removing files or clean\ntemp files\"\n",
|
||||||
|
"title": "High number of inode usage",
|
||||||
|
"value": "96.81%",
|
||||||
|
"device": "/dev/vda1%",
|
||||||
|
"runbook": "troubleshooting/filesystem_alerts_inodes.md"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": "4",
|
||||||
|
"receiver": "vitrage",
|
||||||
|
"externalURL": "http://devstack-rocky-4:9093",
|
||||||
|
"commonLabels": {
|
||||||
|
"severity": "critical",
|
||||||
|
"fstype": "ext4",
|
||||||
|
"instance": "localhost:9100",
|
||||||
|
"job": "node",
|
||||||
|
"alertname": "HighInodeUsage",
|
||||||
|
"device": "/dev/vda1",
|
||||||
|
"mountpoint": "/"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"vitrage_entity_type" : "prometheus",
|
||||||
|
"vitrage_datasource_action" : "update",
|
||||||
|
"vitrage_sample_date": "2018-05-06T06:31:50.094836",
|
||||||
|
"status": "firing",
|
||||||
|
"labels": {
|
||||||
|
"severity": "critical",
|
||||||
|
"fstype": "ext4",
|
||||||
|
"instance": "localhost:9100",
|
||||||
|
"job": "node",
|
||||||
|
"alertname": "HighInodeUsage",
|
||||||
|
"device": "/dev/vda1",
|
||||||
|
"mountpoint": "/"
|
||||||
|
},
|
||||||
|
"endsAt": "0001-01-01T00:00:00Z",
|
||||||
|
"generatorURL": "http://devstack-rocky-4:9090/graph?g0.expr=node_filesystem_files_free%7Bfstype%3D~%22%28ext.%7Cxfs%29%22%2Cjob%3D%22node%22%7D+%2F+node_filesystem_files%7Bfstype%3D~%22%28ext.%7Cxfs%29%22%2Cjob%3D%22node%22%7D+%2A+100+%3C%3D+100&g0.tab=1",
|
||||||
|
"startsAt": "2018-05-03T12:25:38.231388525Z",
|
||||||
|
"annotations": {
|
||||||
|
"mount_point": "/%",
|
||||||
|
"description": "\"Consider ssh\"ing into the instance and removing files or clean\ntemp files\"\n",
|
||||||
|
"title": "High number of inode usage",
|
||||||
|
"value": "96.81%",
|
||||||
|
"device": "/dev/vda1%",
|
||||||
|
"runbook": "troubleshooting/filesystem_alerts_inodes.md"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
# Copyright 2018 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_config import cfg
|
||||||
|
from testtools import matchers
|
||||||
|
|
||||||
|
from vitrage.common.constants import DatasourceProperties as DSProps
|
||||||
|
from vitrage.datasources.prometheus.driver import PROMETHEUS_EVENT_TYPE
|
||||||
|
from vitrage.datasources.prometheus.driver import PrometheusDriver
|
||||||
|
from vitrage.datasources.prometheus import PROMETHEUS_DATASOURCE
|
||||||
|
from vitrage.tests import base
|
||||||
|
from vitrage.tests.mocks import mock_driver
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
class PrometheusDriverTest(base.BaseTest):
|
||||||
|
OPTS = []
|
||||||
|
|
||||||
|
# noinspection PyPep8Naming
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.conf = cfg.ConfigOpts()
|
||||||
|
cls.conf.register_opts(cls.OPTS, group=PROMETHEUS_DATASOURCE)
|
||||||
|
|
||||||
|
def test_enrich_event(self):
|
||||||
|
# Test setup
|
||||||
|
driver = PrometheusDriver(self.conf)
|
||||||
|
event = self._generate_event()
|
||||||
|
|
||||||
|
# Enrich event
|
||||||
|
created_events = driver.enrich_event(event, PROMETHEUS_EVENT_TYPE)
|
||||||
|
|
||||||
|
# Test assertions
|
||||||
|
self._assert_event_equal(created_events, PROMETHEUS_EVENT_TYPE)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_event():
|
||||||
|
generators = mock_driver.simple_prometheus_alarm_generators(
|
||||||
|
update_vals={})
|
||||||
|
|
||||||
|
return mock_driver.generate_sequential_events_list(generators)[0]
|
||||||
|
|
||||||
|
def _assert_event_equal(self,
|
||||||
|
created_events,
|
||||||
|
expected_event_type):
|
||||||
|
self.assertIsNotNone(created_events, 'No events returned')
|
||||||
|
self.assertThat(created_events, matchers.HasLength(1),
|
||||||
|
'Expected one event')
|
||||||
|
self.assertEqual(expected_event_type,
|
||||||
|
created_events[0][DSProps.EVENT_TYPE])
|
@ -0,0 +1,101 @@
|
|||||||
|
# Copyright 2018 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_config import cfg
|
||||||
|
|
||||||
|
from vitrage.common.constants import DatasourceOpts as DSOpts
|
||||||
|
from vitrage.common.constants import DatasourceProperties as DSProps
|
||||||
|
from vitrage.common.constants import UpdateMethod
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE
|
||||||
|
from vitrage.datasources.nova.host.transformer import HostTransformer
|
||||||
|
from vitrage.datasources.prometheus import PROMETHEUS_DATASOURCE
|
||||||
|
from vitrage.datasources.prometheus.properties import get_label
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusAlertStatus \
|
||||||
|
as PAlertStatus
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusLabels \
|
||||||
|
as PLabels
|
||||||
|
from vitrage.datasources.prometheus.properties import PrometheusProperties \
|
||||||
|
as PProps
|
||||||
|
from vitrage.datasources.prometheus.transformer import PrometheusTransformer
|
||||||
|
from vitrage.datasources.transformer_base import TransformerBase
|
||||||
|
from vitrage.tests.mocks import mock_transformer
|
||||||
|
from vitrage.tests.unit.datasources.test_alarm_transformer_base import \
|
||||||
|
BaseAlarmTransformerTest
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
class PrometheusTransformerTest(BaseAlarmTransformerTest):
|
||||||
|
|
||||||
|
OPTS = [
|
||||||
|
cfg.StrOpt(DSOpts.UPDATE_METHOD,
|
||||||
|
default=UpdateMethod.PUSH),
|
||||||
|
]
|
||||||
|
|
||||||
|
# noinspection PyAttributeOutsideInit,PyPep8Naming
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.transformers = {}
|
||||||
|
cls.conf = cfg.ConfigOpts()
|
||||||
|
cls.conf.register_opts(cls.OPTS, group=PROMETHEUS_DATASOURCE)
|
||||||
|
cls.transformers[NOVA_HOST_DATASOURCE] = \
|
||||||
|
HostTransformer(cls.transformers, cls.conf)
|
||||||
|
cls.transformers[PROMETHEUS_DATASOURCE] = \
|
||||||
|
PrometheusTransformer(cls.transformers, cls.conf)
|
||||||
|
|
||||||
|
def test_create_update_entity_vertex(self):
|
||||||
|
# Test setup
|
||||||
|
host1 = 'host1'
|
||||||
|
event = self._generate_event(host1)
|
||||||
|
self.assertIsNotNone(event)
|
||||||
|
|
||||||
|
# Test action
|
||||||
|
transformer = self.transformers[PROMETHEUS_DATASOURCE]
|
||||||
|
wrapper = transformer.transform(event)
|
||||||
|
|
||||||
|
# Test assertions
|
||||||
|
self._validate_vertex_props(wrapper.vertex, event)
|
||||||
|
|
||||||
|
# Validate the neighbors: only one valid host neighbor
|
||||||
|
entity_key1 = transformer._create_entity_key(event)
|
||||||
|
entity_uuid1 = transformer.uuid_from_deprecated_vitrage_id(entity_key1)
|
||||||
|
|
||||||
|
self._validate_host_neighbor(wrapper, entity_uuid1, host1)
|
||||||
|
|
||||||
|
# Validate the expected action on the graph - update or delete
|
||||||
|
self._validate_graph_action(wrapper)
|
||||||
|
|
||||||
|
def _validate_vertex_props(self, vertex, event):
|
||||||
|
self._validate_alarm_vertex_props(
|
||||||
|
vertex, get_label(event, PLabels.ALERT_NAME),
|
||||||
|
PROMETHEUS_DATASOURCE, event[DSProps.SAMPLE_DATE])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _generate_event(hostname):
|
||||||
|
# fake query result to be used by the transformer for determining
|
||||||
|
# the neighbor
|
||||||
|
query_result = [{VProps.VITRAGE_TYPE: NOVA_HOST_DATASOURCE,
|
||||||
|
VProps.ID: hostname}]
|
||||||
|
labels = {PLabels.SEVERITY: 'critical',
|
||||||
|
PLabels.INSTANCE: hostname}
|
||||||
|
|
||||||
|
update_vals = {TransformerBase.QUERY_RESULT: query_result,
|
||||||
|
PProps.LABELS: labels}
|
||||||
|
generators = mock_transformer.simple_prometheus_alarm_generators(
|
||||||
|
update_vals=update_vals)
|
||||||
|
|
||||||
|
return mock_transformer.generate_random_events_list(generators)[0]
|
||||||
|
|
||||||
|
def _is_erroneous(self, vertex):
|
||||||
|
return vertex[PProps.STATUS] == PAlertStatus.FIRING
|
Loading…
Reference in New Issue
Block a user