Merge "Grafana proxy datasource to retrieve metrics"
This commit is contained in:
251
watcher/datasources/grafana.py
Normal file
251
watcher/datasources/grafana.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019 European Organization for Nuclear Research (CERN)
|
||||||
|
#
|
||||||
|
# Authors: Corne Lukken <info@dantalion.nl>
|
||||||
|
#
|
||||||
|
# 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 oslo_log import log
|
||||||
|
import six.moves.urllib.parse as urlparse
|
||||||
|
|
||||||
|
from watcher.common import clients
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.datasources import base
|
||||||
|
from watcher.datasources.grafana_translator import influxdb
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GrafanaHelper(base.DataSourceBase):
|
||||||
|
|
||||||
|
NAME = 'grafana'
|
||||||
|
|
||||||
|
"""METRIC_MAP is only available at runtime _build_metric_map"""
|
||||||
|
METRIC_MAP = dict()
|
||||||
|
|
||||||
|
"""All available translators"""
|
||||||
|
TRANSLATOR_LIST = [
|
||||||
|
influxdb.InfluxDBGrafanaTranslator.NAME
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, osc=None):
|
||||||
|
""":param osc: an OpenStackClients instance"""
|
||||||
|
self.osc = osc if osc else clients.OpenStackClients()
|
||||||
|
self.nova = self.osc.nova()
|
||||||
|
self.configured = False
|
||||||
|
self._base_url = None
|
||||||
|
self._headers = None
|
||||||
|
self._setup()
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
"""Configure grafana helper to perform requests"""
|
||||||
|
|
||||||
|
token = CONF.grafana_client.token
|
||||||
|
base_url = CONF.grafana_client.base_url
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
LOG.critical("GrafanaHelper authentication token not configured")
|
||||||
|
return
|
||||||
|
self._headers = {"Authorization": "Bearer " + token,
|
||||||
|
"Content-Type": "Application/json"}
|
||||||
|
|
||||||
|
if not base_url:
|
||||||
|
LOG.critical("GrafanaHelper url not properly configured, "
|
||||||
|
"check base_url")
|
||||||
|
return
|
||||||
|
self._base_url = base_url
|
||||||
|
|
||||||
|
# Very basic url parsing
|
||||||
|
parse = urlparse.urlparse(self._base_url)
|
||||||
|
if parse.scheme is '' or parse.netloc is '' or parse.path is '':
|
||||||
|
LOG.critical("GrafanaHelper url not properly configured, "
|
||||||
|
"check base_url and project_id")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._build_metric_map()
|
||||||
|
|
||||||
|
if len(self.METRIC_MAP) == 0:
|
||||||
|
LOG.critical("GrafanaHelper not configured for any metrics")
|
||||||
|
|
||||||
|
self.configured = True
|
||||||
|
|
||||||
|
def _build_metric_map(self):
|
||||||
|
"""Builds the metric map by reading config information"""
|
||||||
|
|
||||||
|
for key, value in CONF.grafana_client.database_map.items():
|
||||||
|
try:
|
||||||
|
project = CONF.grafana_client.project_id_map[key]
|
||||||
|
attribute = CONF.grafana_client.attribute_map[key]
|
||||||
|
translator = CONF.grafana_client.translator_map[key]
|
||||||
|
query = CONF.grafana_client.query_map[key]
|
||||||
|
if project is not None and \
|
||||||
|
value is not None and\
|
||||||
|
translator in self.TRANSLATOR_LIST and\
|
||||||
|
query is not None:
|
||||||
|
self.METRIC_MAP[key] = {
|
||||||
|
'db': value,
|
||||||
|
'project': project,
|
||||||
|
'attribute': attribute,
|
||||||
|
'translator': translator,
|
||||||
|
'query': query
|
||||||
|
}
|
||||||
|
except KeyError as e:
|
||||||
|
LOG.error(e)
|
||||||
|
|
||||||
|
def _build_translator_schema(self, metric, db, attribute, query, resource,
|
||||||
|
resource_type, period, aggregate,
|
||||||
|
granularity):
|
||||||
|
"""Create dictionary to pass to grafana proxy translators"""
|
||||||
|
|
||||||
|
return {'metric': metric, 'db': db, 'attribute': attribute,
|
||||||
|
'query': query, 'resource': resource,
|
||||||
|
'resource_type': resource_type, 'period': period,
|
||||||
|
'aggregate': aggregate, 'granularity': granularity}
|
||||||
|
|
||||||
|
def _get_translator(self, name, data):
|
||||||
|
"""Use the names of translators to get the translator for the metric"""
|
||||||
|
if name == influxdb.InfluxDBGrafanaTranslator.NAME:
|
||||||
|
return influxdb.InfluxDBGrafanaTranslator(data)
|
||||||
|
else:
|
||||||
|
raise exception.InvalidParameter(
|
||||||
|
parameter='name', parameter_type='grafana translator')
|
||||||
|
|
||||||
|
def _request(self, params, project_id):
|
||||||
|
"""Make the request to the endpoint to retrieve data
|
||||||
|
|
||||||
|
If the request fails, determines what error to raise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.configured is False:
|
||||||
|
raise exception.DataSourceNotAvailable(self.NAME)
|
||||||
|
|
||||||
|
resp = requests.get(self._base_url + str(project_id) + '/query',
|
||||||
|
params=params, headers=self._headers)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return resp
|
||||||
|
elif resp.status_code == 400:
|
||||||
|
LOG.error("Query for metric is invalid")
|
||||||
|
elif resp.status_code == 401:
|
||||||
|
LOG.error("Authorization token is invalid")
|
||||||
|
raise exception.DataSourceNotAvailable(self.NAME)
|
||||||
|
|
||||||
|
def statistic_aggregation(self, resource=None, resource_type=None,
|
||||||
|
meter_name=None, period=300, aggregate='mean',
|
||||||
|
granularity=300):
|
||||||
|
"""Get the value for the specific metric based on specified parameters
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.METRIC_MAP[meter_name]
|
||||||
|
except KeyError:
|
||||||
|
LOG.error("Metric: {0} does not appear in the current Grafana "
|
||||||
|
"metric map".format(meter_name))
|
||||||
|
raise exception.MetricNotAvailable(metric=meter_name)
|
||||||
|
|
||||||
|
db = self.METRIC_MAP[meter_name]['db']
|
||||||
|
project = self.METRIC_MAP[meter_name]['project']
|
||||||
|
attribute = self.METRIC_MAP[meter_name]['attribute']
|
||||||
|
translator_name = self.METRIC_MAP[meter_name]['translator']
|
||||||
|
query = self.METRIC_MAP[meter_name]['query']
|
||||||
|
|
||||||
|
data = self._build_translator_schema(
|
||||||
|
meter_name, db, attribute, query, resource, resource_type, period,
|
||||||
|
aggregate, granularity)
|
||||||
|
|
||||||
|
translator = self._get_translator(translator_name, data)
|
||||||
|
|
||||||
|
params = translator.build_params()
|
||||||
|
|
||||||
|
raw_kwargs = dict(
|
||||||
|
params=params,
|
||||||
|
project_id=project,
|
||||||
|
)
|
||||||
|
kwargs = {k: v for k, v in raw_kwargs.items() if k and v}
|
||||||
|
|
||||||
|
resp = self.query_retry(self._request, **kwargs)
|
||||||
|
|
||||||
|
result = translator.extract_result(resp.content)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_host_cpu_usage(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'compute_node', 'host_cpu_usage', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_host_ram_usage(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'compute_node', 'host_ram_usage', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_host_outlet_temp(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'compute_node', 'host_outlet_temp', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_host_inlet_temp(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'compute_node', 'host_inlet_temp', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_host_airflow(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'compute_node', 'host_airflow', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_host_power(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'compute_node', 'host_power', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_instance_cpu_usage(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'instance', 'instance_cpu_usage', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_instance_ram_usage(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'instance', 'instance_ram_usage', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_instance_ram_allocated(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'instance', 'instance_ram_allocated', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_instance_l3_cache_usage(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'instance', 'instance_l3_cache_usage', period, aggregate,
|
||||||
|
granularity)
|
||||||
|
|
||||||
|
def get_instance_root_disk_size(self, resource, period=300,
|
||||||
|
aggregate="mean", granularity=None):
|
||||||
|
return self.statistic_aggregation(
|
||||||
|
resource, 'instance', 'instance_root_disk_size', period, aggregate,
|
||||||
|
granularity)
|
||||||
0
watcher/datasources/grafana_translator/__init__.py
Normal file
0
watcher/datasources/grafana_translator/__init__.py
Normal file
125
watcher/datasources/grafana_translator/base.py
Normal file
125
watcher/datasources/grafana_translator/base.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019 European Organization for Nuclear Research (CERN)
|
||||||
|
#
|
||||||
|
# Authors: Corne Lukken <info@dantalion.nl>
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from watcher._i18n import _
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.datasources import base
|
||||||
|
|
||||||
|
|
||||||
|
class BaseGrafanaTranslator(object):
|
||||||
|
"""Grafana translator baseclass to use with grafana for different databases
|
||||||
|
|
||||||
|
Specific databasses that are proxied through grafana require some
|
||||||
|
alterations depending on the database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
data {
|
||||||
|
metric: name of the metric as found in DataSourceBase.METRIC_MAP,
|
||||||
|
db: database specified for this metric in grafana_client config
|
||||||
|
options,
|
||||||
|
attribute: the piece of information that will be selected from the
|
||||||
|
resource object to build the query.
|
||||||
|
query: the unformatted query from the configuration for this metric,
|
||||||
|
resource: the object from the OpenStackClient
|
||||||
|
resource_type: the type of the resource
|
||||||
|
['compute_node','instance', 'bare_metal', 'storage'],
|
||||||
|
period: the period of time to collect metrics for in seconds,
|
||||||
|
aggregate: the aggregation can be any from ['mean', 'max', 'min',
|
||||||
|
'count'],
|
||||||
|
granularity: interval between datapoints in seconds (optional),
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Every grafana translator should have a uniquely identifying name"""
|
||||||
|
NAME = ''
|
||||||
|
|
||||||
|
RESOURCE_TYPES = base.DataSourceBase.RESOURCE_TYPES
|
||||||
|
|
||||||
|
AGGREGATES = base.DataSourceBase.AGGREGATES
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self._data = data
|
||||||
|
self._validate_data()
|
||||||
|
|
||||||
|
def _validate_data(self):
|
||||||
|
"""iterate through the supplied data and verify attributes"""
|
||||||
|
|
||||||
|
optionals = ['granularity']
|
||||||
|
|
||||||
|
reference_data = {
|
||||||
|
'metric': None,
|
||||||
|
'db': None,
|
||||||
|
'attribute': None,
|
||||||
|
'query': None,
|
||||||
|
'resource': None,
|
||||||
|
'resource_type': None,
|
||||||
|
'period': None,
|
||||||
|
'aggregate': None,
|
||||||
|
'granularity': None
|
||||||
|
}
|
||||||
|
reference_data.update(self._data)
|
||||||
|
|
||||||
|
for key, value in reference_data.items():
|
||||||
|
if value is None and key not in optionals:
|
||||||
|
raise exception.InvalidParameter(
|
||||||
|
message=(_("The value %(value)s for parameter "
|
||||||
|
"%(parameter)s is invalid") % {'value': None,
|
||||||
|
'parameter': key}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if reference_data['resource_type'] not in self.RESOURCE_TYPES:
|
||||||
|
raise exception.InvalidParameter(parameter='resource_type',
|
||||||
|
parameter_type='RESOURCE_TYPES')
|
||||||
|
|
||||||
|
if reference_data['aggregate'] not in self.AGGREGATES:
|
||||||
|
raise exception.InvalidParameter(parameter='aggregate',
|
||||||
|
parameter_type='AGGREGATES')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_attribute(resource, attribute):
|
||||||
|
"""Retrieve the desired attribute from the resource
|
||||||
|
|
||||||
|
:param resource: The resource object to extract the attribute from.
|
||||||
|
:param attribute: The name of the attribute to subtract as string.
|
||||||
|
:return: The extracted attribute or None
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return getattr(resource, attribute)
|
||||||
|
except AttributeError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _query_format(query, aggregate, resource, period,
|
||||||
|
granularity, translator_specific):
|
||||||
|
return query.format(aggregate, resource, period, granularity,
|
||||||
|
translator_specific)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def build_params(self):
|
||||||
|
"""Build the set of parameters to send with the request"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def extract_result(self, raw_results):
|
||||||
|
"""Extrapolate the metric from the raw results of the request"""
|
||||||
|
raise NotImplementedError()
|
||||||
87
watcher/datasources/grafana_translator/influxdb.py
Normal file
87
watcher/datasources/grafana_translator/influxdb.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019 European Organization for Nuclear Research (CERN)
|
||||||
|
#
|
||||||
|
# Authors: Corne Lukken <info@dantalion.nl>
|
||||||
|
#
|
||||||
|
# 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 oslo_log import log
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.datasources.grafana_translator.base import BaseGrafanaTranslator
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InfluxDBGrafanaTranslator(BaseGrafanaTranslator):
|
||||||
|
"""Grafana translator to communicate with InfluxDB database"""
|
||||||
|
|
||||||
|
NAME = 'influxdb'
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
super(InfluxDBGrafanaTranslator, self).__init__(data)
|
||||||
|
|
||||||
|
def build_params(self):
|
||||||
|
""""""
|
||||||
|
|
||||||
|
data = self._data
|
||||||
|
|
||||||
|
retention_period = None
|
||||||
|
available_periods = CONF.grafana_translators.retention_periods.items()
|
||||||
|
for key, value in sorted(available_periods, key=lambda x: x[1]):
|
||||||
|
if int(data['period']) < int(value):
|
||||||
|
retention_period = key
|
||||||
|
break
|
||||||
|
|
||||||
|
if retention_period is None:
|
||||||
|
retention_period = max(available_periods)[0]
|
||||||
|
LOG.warning("Longest retention period is to short for desired"
|
||||||
|
" period")
|
||||||
|
|
||||||
|
try:
|
||||||
|
resource = self._extract_attribute(
|
||||||
|
data['resource'], data['attribute'])
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error("Resource: {0} does not contain attribute {1}".format(
|
||||||
|
data['resource'], data['attribute']))
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Granularity is optional if it is None the minimal value for InfluxDB
|
||||||
|
# will be 1
|
||||||
|
granularity = \
|
||||||
|
data['granularity'] if data['granularity'] is not None else 1
|
||||||
|
|
||||||
|
return {'db': data['db'],
|
||||||
|
'epoch': 'ms',
|
||||||
|
'q': self._query_format(
|
||||||
|
data['query'], data['aggregate'], resource, data['period'],
|
||||||
|
granularity, retention_period)}
|
||||||
|
|
||||||
|
def extract_result(self, raw_results):
|
||||||
|
""""""
|
||||||
|
try:
|
||||||
|
# For result structure see:
|
||||||
|
# https://docs.openstack.org/watcher/latest/datasources/grafana.html#InfluxDB
|
||||||
|
result = jsonutils.loads(raw_results)
|
||||||
|
result = result['results'][0]['series'][0]
|
||||||
|
index_aggregate = result['columns'].index(self._data['aggregate'])
|
||||||
|
return result['values'][0][index_aggregate]
|
||||||
|
except KeyError:
|
||||||
|
LOG.error("Could not extract {0} for the resource: {1}".format(
|
||||||
|
self._data['metric'], self._data['resource']))
|
||||||
|
raise exception.NoSuchMetricForHost(
|
||||||
|
metric=self._data['metric'], host=self._data['resource'])
|
||||||
@@ -23,6 +23,7 @@ from oslo_log import log
|
|||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.datasources import ceilometer as ceil
|
from watcher.datasources import ceilometer as ceil
|
||||||
from watcher.datasources import gnocchi as gnoc
|
from watcher.datasources import gnocchi as gnoc
|
||||||
|
from watcher.datasources import grafana as graf
|
||||||
from watcher.datasources import monasca as mon
|
from watcher.datasources import monasca as mon
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@@ -34,6 +35,7 @@ class DataSourceManager(object):
|
|||||||
(gnoc.GnocchiHelper.NAME, gnoc.GnocchiHelper.METRIC_MAP),
|
(gnoc.GnocchiHelper.NAME, gnoc.GnocchiHelper.METRIC_MAP),
|
||||||
(ceil.CeilometerHelper.NAME, ceil.CeilometerHelper.METRIC_MAP),
|
(ceil.CeilometerHelper.NAME, ceil.CeilometerHelper.METRIC_MAP),
|
||||||
(mon.MonascaHelper.NAME, mon.MonascaHelper.METRIC_MAP),
|
(mon.MonascaHelper.NAME, mon.MonascaHelper.METRIC_MAP),
|
||||||
|
(graf.GrafanaHelper.NAME, graf.GrafanaHelper.METRIC_MAP),
|
||||||
])
|
])
|
||||||
"""Dictionary with all possible datasources, dictionary order is the default
|
"""Dictionary with all possible datasources, dictionary order is the default
|
||||||
order for attempting to use datasources
|
order for attempting to use datasources
|
||||||
@@ -45,6 +47,11 @@ class DataSourceManager(object):
|
|||||||
self._ceilometer = None
|
self._ceilometer = None
|
||||||
self._monasca = None
|
self._monasca = None
|
||||||
self._gnocchi = None
|
self._gnocchi = None
|
||||||
|
self._grafana = None
|
||||||
|
|
||||||
|
# Dynamically update grafana metric map, only available at runtime
|
||||||
|
# The metric map can still be overridden by a yaml config file
|
||||||
|
self.metric_map[graf.GrafanaHelper.NAME] = self.grafana.METRIC_MAP
|
||||||
|
|
||||||
metric_map_path = cfg.CONF.watcher_decision_engine.metric_map_path
|
metric_map_path = cfg.CONF.watcher_decision_engine.metric_map_path
|
||||||
metrics_from_file = self.load_metric_map(metric_map_path)
|
metrics_from_file = self.load_metric_map(metric_map_path)
|
||||||
@@ -54,6 +61,7 @@ class DataSourceManager(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
msgargs = (ds, self.metric_map.keys())
|
msgargs = (ds, self.metric_map.keys())
|
||||||
LOG.warning('Invalid Datasource: %s. Allowed: %s ', *msgargs)
|
LOG.warning('Invalid Datasource: %s. Allowed: %s ', *msgargs)
|
||||||
|
|
||||||
self.datasources = self.config.datasources
|
self.datasources = self.config.datasources
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -86,6 +94,16 @@ class DataSourceManager(object):
|
|||||||
def gnocchi(self, gnocchi):
|
def gnocchi(self, gnocchi):
|
||||||
self._gnocchi = gnocchi
|
self._gnocchi = gnocchi
|
||||||
|
|
||||||
|
@property
|
||||||
|
def grafana(self):
|
||||||
|
if self._grafana is None:
|
||||||
|
self._grafana = graf.GrafanaHelper(osc=self.osc)
|
||||||
|
return self._grafana
|
||||||
|
|
||||||
|
@grafana.setter
|
||||||
|
def grafana(self, grafana):
|
||||||
|
self._grafana = grafana
|
||||||
|
|
||||||
def get_backend(self, metrics):
|
def get_backend(self, metrics):
|
||||||
"""Determine the datasource to use from the configuration
|
"""Determine the datasource to use from the configuration
|
||||||
|
|
||||||
|
|||||||
105
watcher/tests/datasources/grafana_translators/test_base.py
Normal file
105
watcher/tests/datasources/grafana_translators/test_base.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019 European Organization for Nuclear Research (CERN)
|
||||||
|
#
|
||||||
|
# Authors: Corne Lukken <info@dantalion.nl>
|
||||||
|
#
|
||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.datasources.grafana_translator import base as base_translator
|
||||||
|
from watcher.tests import base
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGrafanaTranslatorBase(base.BaseTestCase):
|
||||||
|
"""Base class for all GrafanaTranslator test classes
|
||||||
|
|
||||||
|
Objects under test are preceded with t_ and mocked objects are preceded
|
||||||
|
with m_ , additionally, patched objects are preceded with p_ no object
|
||||||
|
under test should be created in setUp this can influence the results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestGrafanaTranslatorBase, self).setUp()
|
||||||
|
|
||||||
|
"""Basic valid reference data"""
|
||||||
|
self.reference_data = {
|
||||||
|
'metric': 'host_cpu_usage',
|
||||||
|
'db': 'production',
|
||||||
|
'attribute': 'hostname',
|
||||||
|
'query': 'SHOW all_base FROM belong_to_us',
|
||||||
|
'resource': mock.Mock(hostname='hyperion'),
|
||||||
|
'resource_type': 'compute_node',
|
||||||
|
'period': '120',
|
||||||
|
'aggregate': 'mean',
|
||||||
|
'granularity': None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseGrafanaTranslator(TestGrafanaTranslatorBase):
|
||||||
|
"""Test the GrafanaTranslator base class
|
||||||
|
|
||||||
|
Objects under test are preceded with t_ and mocked objects are preceded
|
||||||
|
with m_ , additionally, patched objects are preceded with p_ no object
|
||||||
|
under test should be created in setUp this can influence the results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestBaseGrafanaTranslator, self).setUp()
|
||||||
|
|
||||||
|
def test_validate_data(self):
|
||||||
|
"""Initialize InfluxDBGrafanaTranslator and check data validation"""
|
||||||
|
|
||||||
|
t_base_translator = base_translator.BaseGrafanaTranslator(
|
||||||
|
data=self.reference_data)
|
||||||
|
|
||||||
|
self.assertIsInstance(t_base_translator,
|
||||||
|
base_translator.BaseGrafanaTranslator)
|
||||||
|
|
||||||
|
def test_validate_data_error(self):
|
||||||
|
"""Initialize InfluxDBGrafanaTranslator and check data validation"""
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidParameter,
|
||||||
|
base_translator.BaseGrafanaTranslator,
|
||||||
|
data=[])
|
||||||
|
|
||||||
|
def test_extract_attribute(self):
|
||||||
|
"""Test that an attribute can be extracted from an object"""
|
||||||
|
|
||||||
|
m_object = mock.Mock(hostname='test')
|
||||||
|
|
||||||
|
t_base_translator = base_translator.BaseGrafanaTranslator(
|
||||||
|
data=self.reference_data)
|
||||||
|
|
||||||
|
self.assertEqual('test', t_base_translator._extract_attribute(
|
||||||
|
m_object, 'hostname'))
|
||||||
|
|
||||||
|
def test_extract_attribute_error(self):
|
||||||
|
"""Test error on attempt to extract none existing attribute"""
|
||||||
|
|
||||||
|
m_object = mock.Mock(hostname='test')
|
||||||
|
m_object.test = mock.PropertyMock(side_effect=AttributeError)
|
||||||
|
|
||||||
|
t_base_translator = base_translator.BaseGrafanaTranslator(
|
||||||
|
data=self.reference_data)
|
||||||
|
|
||||||
|
self.assertRaises(AttributeError, t_base_translator._extract_attribute(
|
||||||
|
m_object, 'test'))
|
||||||
175
watcher/tests/datasources/grafana_translators/test_influxdb.py
Normal file
175
watcher/tests/datasources/grafana_translators/test_influxdb.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019 European Organization for Nuclear Research (CERN)
|
||||||
|
#
|
||||||
|
# Authors: Corne Lukken <info@dantalion.nl>
|
||||||
|
#
|
||||||
|
# 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 copy
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.datasources.grafana_translator import influxdb
|
||||||
|
from watcher.tests.datasources.grafana_translators import test_base
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInfluxDBGrafanaTranslator(test_base.TestGrafanaTranslatorBase):
|
||||||
|
"""Test the InfluxDB gragana database translator
|
||||||
|
|
||||||
|
Objects under test are preceded with t_ and mocked objects are preceded
|
||||||
|
with m_ , additionally, patched objects are preceded with p_ no object
|
||||||
|
under test should be created in setUp this can influence the results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestInfluxDBGrafanaTranslator, self).setUp()
|
||||||
|
|
||||||
|
self.p_conf = mock.patch.object(
|
||||||
|
influxdb, 'CONF',
|
||||||
|
new_callable=mock.PropertyMock)
|
||||||
|
self.m_conf = self.p_conf.start()
|
||||||
|
self.addCleanup(self.p_conf.stop)
|
||||||
|
|
||||||
|
self.m_conf.grafana_translators.retention_periods = {
|
||||||
|
'one_day': 86400,
|
||||||
|
'one_week': 604800
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_retention_period_one_day(self):
|
||||||
|
"""Validate lowest retention period"""
|
||||||
|
|
||||||
|
data = copy.copy(self.reference_data)
|
||||||
|
data['query'] = "{4}"
|
||||||
|
|
||||||
|
t_influx = influxdb.InfluxDBGrafanaTranslator(
|
||||||
|
data=data)
|
||||||
|
params = t_influx.build_params()
|
||||||
|
self.assertEqual(params['q'], 'one_day')
|
||||||
|
|
||||||
|
def test_retention_period_one_week(self):
|
||||||
|
"""Validate incrementing retention periods"""
|
||||||
|
|
||||||
|
data = copy.copy(self.reference_data)
|
||||||
|
data['query'] = "{4}"
|
||||||
|
|
||||||
|
data['period'] = 90000
|
||||||
|
t_influx = influxdb.InfluxDBGrafanaTranslator(
|
||||||
|
data=data)
|
||||||
|
params = t_influx.build_params()
|
||||||
|
self.assertEqual(params['q'], 'one_week')
|
||||||
|
|
||||||
|
@mock.patch.object(influxdb, 'LOG')
|
||||||
|
def test_retention_period_warning(self, m_log):
|
||||||
|
"""Validate retention period warning"""
|
||||||
|
|
||||||
|
data = copy.copy(self.reference_data)
|
||||||
|
data['query'] = "{4}"
|
||||||
|
|
||||||
|
data['period'] = 650000
|
||||||
|
t_influx = influxdb.InfluxDBGrafanaTranslator(
|
||||||
|
data=data)
|
||||||
|
params = t_influx.build_params()
|
||||||
|
self.assertEqual(params['q'], 'one_week')
|
||||||
|
m_log.warning.assert_called_once_with(
|
||||||
|
"Longest retention period is to short for desired period")
|
||||||
|
|
||||||
|
def test_build_params_granularity(self):
|
||||||
|
"""Validate build params granularity"""
|
||||||
|
|
||||||
|
data = copy.copy(self.reference_data)
|
||||||
|
data['granularity'] = None
|
||||||
|
data['query'] = "{3}"
|
||||||
|
|
||||||
|
t_influx = influxdb.InfluxDBGrafanaTranslator(
|
||||||
|
data=data)
|
||||||
|
|
||||||
|
raw_results = {
|
||||||
|
'db': 'production',
|
||||||
|
'epoch': 'ms',
|
||||||
|
'q': '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
# InfluxDB build_params should replace granularity None optional with 1
|
||||||
|
result = t_influx.build_params()
|
||||||
|
|
||||||
|
self.assertEqual(raw_results, result)
|
||||||
|
|
||||||
|
def test_build_params_order(self):
|
||||||
|
"""Validate order of build params"""
|
||||||
|
|
||||||
|
data = copy.copy(self.reference_data)
|
||||||
|
data['aggregate'] = 'count'
|
||||||
|
# prevent having to deepcopy by keeping this value the same
|
||||||
|
# this will access the value 'hyperion' from the mocked resource object
|
||||||
|
data['attribute'] = 'hostname'
|
||||||
|
data['period'] = 3
|
||||||
|
# because the period is only 3 the retention_period will be one_day
|
||||||
|
data['granularity'] = 4
|
||||||
|
data['query'] = "{0}{1}{2}{3}{4}"
|
||||||
|
|
||||||
|
t_influx = influxdb.InfluxDBGrafanaTranslator(
|
||||||
|
data=data)
|
||||||
|
|
||||||
|
raw_results = "counthyperion34one_day"
|
||||||
|
|
||||||
|
result = t_influx.build_params()
|
||||||
|
|
||||||
|
self.assertEqual(raw_results, result['q'])
|
||||||
|
|
||||||
|
def test_extract_results(self):
|
||||||
|
"""Validate proper result extraction"""
|
||||||
|
|
||||||
|
t_influx = influxdb.InfluxDBGrafanaTranslator(
|
||||||
|
data=self.reference_data)
|
||||||
|
|
||||||
|
raw_results = "{ \"results\": [{ \"series\": [{ " \
|
||||||
|
"\"columns\": [\"time\",\"mean\"]," \
|
||||||
|
"\"values\": [[1552500855000, " \
|
||||||
|
"67.3550078657577]]}]}]}"
|
||||||
|
|
||||||
|
# Structure of InfluxDB time series data
|
||||||
|
# { "results": [{
|
||||||
|
# "statement_id": 0,
|
||||||
|
# "series": [{
|
||||||
|
# "name": "cpu_percent",
|
||||||
|
# "columns": [
|
||||||
|
# "time",
|
||||||
|
# "mean"
|
||||||
|
# ],
|
||||||
|
# "values": [[
|
||||||
|
# 1552500855000,
|
||||||
|
# 67.3550078657577
|
||||||
|
# ]]
|
||||||
|
# }]
|
||||||
|
# }]}
|
||||||
|
|
||||||
|
self.assertEqual(t_influx.extract_result(raw_results),
|
||||||
|
67.3550078657577)
|
||||||
|
|
||||||
|
def test_extract_results_error(self):
|
||||||
|
"""Validate error on missing results"""
|
||||||
|
|
||||||
|
t_influx = influxdb.InfluxDBGrafanaTranslator(
|
||||||
|
data=self.reference_data)
|
||||||
|
|
||||||
|
raw_results = "{}"
|
||||||
|
|
||||||
|
self.assertRaises(exception.NoSuchMetricForHost,
|
||||||
|
t_influx.extract_result, raw_results)
|
||||||
305
watcher/tests/datasources/test_grafana_helper.py
Normal file
305
watcher/tests/datasources/test_grafana_helper.py
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019 European Organization for Nuclear Research (CERN)
|
||||||
|
#
|
||||||
|
# Authors: Corne Lukken <info@dantalion.nl>
|
||||||
|
#
|
||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from watcher.common import clients
|
||||||
|
from watcher.common import exception
|
||||||
|
from watcher.datasources import grafana
|
||||||
|
from watcher.tests import base
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(clients.OpenStackClients, 'nova', mock.Mock())
|
||||||
|
class TestGrafana(base.BaseTestCase):
|
||||||
|
"""Test the GrafanaHelper datasource
|
||||||
|
|
||||||
|
Objects under test are preceded with t_ and mocked objects are preceded
|
||||||
|
with m_ , additionally, patched objects are preceded with p_ no object
|
||||||
|
under test should be created in setUp this can influence the results.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestGrafana, self).setUp()
|
||||||
|
|
||||||
|
self.p_conf = mock.patch.object(
|
||||||
|
grafana, 'CONF',
|
||||||
|
new_callable=mock.PropertyMock)
|
||||||
|
self.m_conf = self.p_conf.start()
|
||||||
|
self.addCleanup(self.p_conf.stop)
|
||||||
|
|
||||||
|
self.m_conf.grafana_client.token = \
|
||||||
|
"eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk=="
|
||||||
|
self.m_conf.grafana_client.base_url = "https://grafana.proxy/api/"
|
||||||
|
self.m_conf.grafana_client.project_id_map = {'host_cpu_usage': 7221}
|
||||||
|
self.m_conf.grafana_client.database_map = \
|
||||||
|
{'host_cpu_usage': 'mock_db'}
|
||||||
|
self.m_conf.grafana_client.attribute_map = \
|
||||||
|
{'host_cpu_usage': 'hostname'}
|
||||||
|
self.m_conf.grafana_client.translator_map = \
|
||||||
|
{'host_cpu_usage': 'influxdb'}
|
||||||
|
self.m_conf.grafana_client.query_map = \
|
||||||
|
{'host_cpu_usage': 'SELECT 100-{0}("{0}_value") FROM {3}.'
|
||||||
|
'cpu_percent WHERE ("host" =~ /^{1}$/ AND '
|
||||||
|
'"type_instance" =~/^idle$/ AND time > '
|
||||||
|
'(now()-{2}m)'}
|
||||||
|
|
||||||
|
self.m_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
stat_agg_patcher = mock.patch.object(
|
||||||
|
self.m_grafana, 'statistic_aggregation',
|
||||||
|
spec=grafana.GrafanaHelper.statistic_aggregation)
|
||||||
|
self.mock_aggregation = stat_agg_patcher.start()
|
||||||
|
self.addCleanup(stat_agg_patcher.stop)
|
||||||
|
|
||||||
|
self.m_compute_node = mock.Mock(
|
||||||
|
id='16a86790-327a-45f9-bc82-45839f062fdc',
|
||||||
|
hostname='example.hostname.ch'
|
||||||
|
)
|
||||||
|
self.m_instance = mock.Mock(
|
||||||
|
id='73b1ff78-aca7-404f-ac43-3ed16c1fa555',
|
||||||
|
human_id='example.hostname'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_configured(self):
|
||||||
|
"""Initialize GrafanaHelper and check if configured is true"""
|
||||||
|
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
self.assertTrue(t_grafana.configured)
|
||||||
|
|
||||||
|
def test_configured_error(self):
|
||||||
|
"""Butcher the required configuration and test if configured is false
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.m_conf.grafana_client.base_url = ""
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
self.assertFalse(t_grafana.configured)
|
||||||
|
|
||||||
|
def test_configured_raise_error(self):
|
||||||
|
"""Test raising error when using improperly configured GrafanHelper
|
||||||
|
|
||||||
|
Assure that the _get_metric method raises errors if the metric is
|
||||||
|
missing from the map
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Clear the METRIC_MAP of Grafana since it is a static variable that
|
||||||
|
# other tests might have set before this test runs.
|
||||||
|
grafana.GrafanaHelper.METRIC_MAP = {}
|
||||||
|
|
||||||
|
self.m_conf.grafana_client.base_url = ""
|
||||||
|
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
|
||||||
|
self.assertFalse(t_grafana.configured)
|
||||||
|
self.assertEqual({}, t_grafana.METRIC_MAP)
|
||||||
|
self.assertRaises(
|
||||||
|
exception.MetricNotAvailable,
|
||||||
|
t_grafana.get_host_cpu_usage,
|
||||||
|
self.m_compute_node
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch.object(requests, 'get')
|
||||||
|
def test_request_raise_error(self, m_request):
|
||||||
|
"""Test raising error when status code of request indicates problem
|
||||||
|
|
||||||
|
Assure that the _request method raises errors if the response indicates
|
||||||
|
problems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
m_request.return_value = mock.Mock(status_code=404)
|
||||||
|
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
exception.DataSourceNotAvailable,
|
||||||
|
t_grafana.get_host_cpu_usage,
|
||||||
|
self.m_compute_node
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_metric_raise_error(self):
|
||||||
|
"""Test raising error when specified meter does not exist"""
|
||||||
|
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
|
||||||
|
self.assertRaises(exception.MetricNotAvailable,
|
||||||
|
t_grafana.statistic_aggregation,
|
||||||
|
self.m_compute_node,
|
||||||
|
'none existing meter', 60)
|
||||||
|
|
||||||
|
@mock.patch.object(grafana.GrafanaHelper, '_request')
|
||||||
|
def test_get_metric_raise_error(self, m_request):
|
||||||
|
"""Test raising error when endpoint unable to deliver data for metric
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
m_request.return_value.content = "{}"
|
||||||
|
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
|
||||||
|
self.assertRaises(exception.NoSuchMetricForHost,
|
||||||
|
t_grafana.get_host_cpu_usage,
|
||||||
|
self.m_compute_node, 60)
|
||||||
|
|
||||||
|
def test_metric_builder(self):
|
||||||
|
"""Creates valid and invalid sets of configuration for metrics
|
||||||
|
|
||||||
|
Ensures that a valid metric entry can be configured even if multiple
|
||||||
|
invalid configurations exist for other metrics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.m_conf.grafana_client.project_id_map = {
|
||||||
|
'host_cpu_usage': 7221,
|
||||||
|
'host_ram_usage': 7221,
|
||||||
|
'instance_ram_allocated': 7221,
|
||||||
|
}
|
||||||
|
self.m_conf.grafana_client.database_map = {
|
||||||
|
'host_cpu_usage': 'mock_db',
|
||||||
|
'instance_cpu_usage': 'mock_db',
|
||||||
|
'instance_ram_allocated': 'mock_db',
|
||||||
|
}
|
||||||
|
self.m_conf.grafana_client.attribute_map = {
|
||||||
|
'host_cpu_usage': 'hostname',
|
||||||
|
'host_power': 'hostname',
|
||||||
|
'instance_ram_allocated': 'human_id',
|
||||||
|
}
|
||||||
|
self.m_conf.grafana_client.translator_map = {
|
||||||
|
'host_cpu_usage': 'influxdb',
|
||||||
|
'host_inlet_temp': 'influxdb',
|
||||||
|
# validate that invalid entries don't get added
|
||||||
|
'instance_ram_usage': 'dummy',
|
||||||
|
'instance_ram_allocated': 'influxdb',
|
||||||
|
}
|
||||||
|
self.m_conf.grafana_client.query_map = {
|
||||||
|
'host_cpu_usage': 'SHOW SERIES',
|
||||||
|
'instance_ram_usage': 'SHOW SERIES',
|
||||||
|
'instance_ram_allocated': 'SHOW SERIES',
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_result = {
|
||||||
|
'host_cpu_usage': {
|
||||||
|
'db': 'mock_db',
|
||||||
|
'project': 7221,
|
||||||
|
'attribute': 'hostname',
|
||||||
|
'translator': 'influxdb',
|
||||||
|
'query': 'SHOW SERIES'},
|
||||||
|
'instance_ram_allocated': {
|
||||||
|
'db': 'mock_db',
|
||||||
|
'project': 7221,
|
||||||
|
'attribute': 'human_id',
|
||||||
|
'translator': 'influxdb',
|
||||||
|
'query': 'SHOW SERIES'},
|
||||||
|
}
|
||||||
|
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
self.assertEqual(t_grafana.METRIC_MAP, expected_result)
|
||||||
|
|
||||||
|
@mock.patch.object(grafana.GrafanaHelper, '_request')
|
||||||
|
def test_statistic_aggregation(self, m_request):
|
||||||
|
m_request.return_value.content = "{ \"results\": [{ \"series\": [{ " \
|
||||||
|
"\"columns\": [\"time\",\"mean\"]," \
|
||||||
|
"\"values\": [[1552500855000, " \
|
||||||
|
"67.3550078657577]]}]}]}"
|
||||||
|
|
||||||
|
t_grafana = grafana.GrafanaHelper(osc=mock.Mock())
|
||||||
|
|
||||||
|
result = t_grafana.statistic_aggregation(
|
||||||
|
self.m_compute_node, 'compute_node', 'host_cpu_usage', 60)
|
||||||
|
self.assertEqual(result, 67.3550078657577)
|
||||||
|
|
||||||
|
def test_get_host_cpu_usage(self):
|
||||||
|
self.m_grafana.get_host_cpu_usage(self.m_compute_node, 60, 'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'compute_node', 'host_cpu_usage', 60, 'min',
|
||||||
|
15)
|
||||||
|
|
||||||
|
def test_get_host_ram_usage(self):
|
||||||
|
self.m_grafana.get_host_ram_usage(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'compute_node', 'host_ram_usage', 60, 'min',
|
||||||
|
15)
|
||||||
|
|
||||||
|
def test_get_host_outlet_temperature(self):
|
||||||
|
self.m_grafana.get_host_outlet_temp(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'compute_node', 'host_outlet_temp', 60, 'min',
|
||||||
|
15)
|
||||||
|
|
||||||
|
def test_get_host_inlet_temperature(self):
|
||||||
|
self.m_grafana.get_host_inlet_temp(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'compute_node', 'host_inlet_temp', 60, 'min',
|
||||||
|
15)
|
||||||
|
|
||||||
|
def test_get_host_airflow(self):
|
||||||
|
self.m_grafana.get_host_airflow(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'compute_node', 'host_airflow', 60, 'min',
|
||||||
|
15)
|
||||||
|
|
||||||
|
def test_get_host_power(self):
|
||||||
|
self.m_grafana.get_host_power(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'compute_node', 'host_power', 60, 'min',
|
||||||
|
15)
|
||||||
|
|
||||||
|
def test_get_instance_cpu_usage(self):
|
||||||
|
self.m_grafana.get_instance_cpu_usage(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'instance', 'instance_cpu_usage', 60,
|
||||||
|
'min', 15)
|
||||||
|
|
||||||
|
def test_get_instance_ram_usage(self):
|
||||||
|
self.m_grafana.get_instance_ram_usage(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'instance', 'instance_ram_usage', 60,
|
||||||
|
'min', 15)
|
||||||
|
|
||||||
|
def test_get_instance_ram_allocated(self):
|
||||||
|
self.m_grafana.get_instance_ram_allocated(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'instance', 'instance_ram_allocated', 60,
|
||||||
|
'min', 15)
|
||||||
|
|
||||||
|
def test_get_instance_l3_cache_usage(self):
|
||||||
|
self.m_grafana.get_instance_l3_cache_usage(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'instance', 'instance_l3_cache_usage', 60,
|
||||||
|
'min', 15)
|
||||||
|
|
||||||
|
def test_get_instance_root_disk_allocated(self):
|
||||||
|
self.m_grafana.get_instance_root_disk_size(self.m_compute_node, 60,
|
||||||
|
'min', 15)
|
||||||
|
self.mock_aggregation.assert_called_once_with(
|
||||||
|
self.m_compute_node, 'instance', 'instance_root_disk_size', 60,
|
||||||
|
'min', 15)
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import six
|
|
||||||
|
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
|
|
||||||
from watcher.common import exception
|
from watcher.common import exception
|
||||||
from watcher.datasources import gnocchi
|
from watcher.datasources import gnocchi
|
||||||
|
from watcher.datasources import grafana
|
||||||
from watcher.datasources import manager as ds_manager
|
from watcher.datasources import manager as ds_manager
|
||||||
from watcher.datasources import monasca
|
from watcher.datasources import monasca
|
||||||
from watcher.tests import base
|
from watcher.tests import base
|
||||||
@@ -57,6 +57,31 @@ class TestDataSourceManager(base.BaseTestCase):
|
|||||||
backend = manager.get_backend(['host_airflow'])
|
backend = manager.get_backend(['host_airflow'])
|
||||||
self.assertEqual("host_fnspid", backend.METRIC_MAP['host_airflow'])
|
self.assertEqual("host_fnspid", backend.METRIC_MAP['host_airflow'])
|
||||||
|
|
||||||
|
@mock.patch.object(grafana, 'CONF')
|
||||||
|
def test_metric_file_metric_override_grafana(self, m_config):
|
||||||
|
"""Grafana requires a different structure in the metric map"""
|
||||||
|
|
||||||
|
m_config.grafana_client.token = \
|
||||||
|
"eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk=="
|
||||||
|
m_config.grafana_client.base_url = "https://grafana.proxy/api/"
|
||||||
|
|
||||||
|
path = 'watcher.datasources.manager.DataSourceManager.load_metric_map'
|
||||||
|
metric_map = {
|
||||||
|
'db': 'production_cloud',
|
||||||
|
'project': '7485',
|
||||||
|
'attribute': 'hostname',
|
||||||
|
'translator': 'influxdb',
|
||||||
|
'query': 'SHOW SERIES'
|
||||||
|
}
|
||||||
|
retval = {
|
||||||
|
grafana.GrafanaHelper.NAME: {"host_airflow": metric_map}
|
||||||
|
}
|
||||||
|
with mock.patch(path, return_value=retval):
|
||||||
|
dsmcfg = self._dsm_config(datasources=['grafana'])
|
||||||
|
manager = self._dsm(config=dsmcfg)
|
||||||
|
backend = manager.get_backend(['host_airflow'])
|
||||||
|
self.assertEqual(metric_map, backend.METRIC_MAP['host_airflow'])
|
||||||
|
|
||||||
def test_metric_file_invalid_ds(self):
|
def test_metric_file_invalid_ds(self):
|
||||||
with mock.patch('yaml.safe_load') as mo:
|
with mock.patch('yaml.safe_load') as mo:
|
||||||
mo.return_value = {"newds": {"metric_one": "i_am_metric_one"}}
|
mo.return_value = {"newds": {"metric_one": "i_am_metric_one"}}
|
||||||
@@ -77,10 +102,8 @@ class TestDataSourceManager(base.BaseTestCase):
|
|||||||
|
|
||||||
def test_get_backend_wrong_metric(self):
|
def test_get_backend_wrong_metric(self):
|
||||||
manager = self._dsm()
|
manager = self._dsm()
|
||||||
ex = self.assertRaises(
|
self.assertRaises(exception.MetricNotAvailable, manager.get_backend,
|
||||||
exception.MetricNotAvailable, manager.get_backend,
|
['host_cpu', 'instance_cpu_usage'])
|
||||||
['host_cpu', 'instance_cpu_usage'])
|
|
||||||
self.assertIn('Metric: host_cpu not available', six.text_type(ex))
|
|
||||||
|
|
||||||
@mock.patch.object(gnocchi, 'GnocchiHelper')
|
@mock.patch.object(gnocchi, 'GnocchiHelper')
|
||||||
def test_get_backend_error_datasource(self, m_gnocchi):
|
def test_get_backend_error_datasource(self, m_gnocchi):
|
||||||
@@ -89,6 +112,33 @@ class TestDataSourceManager(base.BaseTestCase):
|
|||||||
backend = manager.get_backend(['host_cpu_usage', 'instance_cpu_usage'])
|
backend = manager.get_backend(['host_cpu_usage', 'instance_cpu_usage'])
|
||||||
self.assertEqual(backend, manager.ceilometer)
|
self.assertEqual(backend, manager.ceilometer)
|
||||||
|
|
||||||
|
@mock.patch.object(grafana.GrafanaHelper, 'METRIC_MAP',
|
||||||
|
{'host_cpu_usage': 'test'})
|
||||||
|
def test_get_backend_grafana(self):
|
||||||
|
dss = ['grafana', 'ceilometer', 'gnocchi']
|
||||||
|
dsmcfg = self._dsm_config(datasources=dss)
|
||||||
|
manager = self._dsm(config=dsmcfg)
|
||||||
|
backend = manager.get_backend(['host_cpu_usage'])
|
||||||
|
self.assertEqual(backend, manager.grafana)
|
||||||
|
|
||||||
|
@mock.patch.object(grafana, 'CONF')
|
||||||
|
def test_dynamic_metric_map_grafana(self, m_config):
|
||||||
|
m_config.grafana_client.token = \
|
||||||
|
"eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk=="
|
||||||
|
m_config.grafana_client.base_url = "https://grafana.proxy/api/"
|
||||||
|
m_config.grafana_client.project_id_map = {'host_cpu_usage': 7221}
|
||||||
|
m_config.grafana_client.attribute_map = {'host_cpu_usage': 'hostname'}
|
||||||
|
m_config.grafana_client.database_map = {'host_cpu_usage': 'mock_db'}
|
||||||
|
m_config.grafana_client.translator_map = {'host_cpu_usage': 'influxdb'}
|
||||||
|
m_config.grafana_client.query_map = {
|
||||||
|
'host_cpu_usage': 'SHOW SERIES'
|
||||||
|
}
|
||||||
|
dss = ['grafana', 'ceilometer', 'gnocchi']
|
||||||
|
dsmcfg = self._dsm_config(datasources=dss)
|
||||||
|
manager = self._dsm(config=dsmcfg)
|
||||||
|
backend = manager.get_backend(['host_cpu_usage'])
|
||||||
|
self.assertEqual(backend, manager.grafana)
|
||||||
|
|
||||||
def test_get_backend_no_datasources(self):
|
def test_get_backend_no_datasources(self):
|
||||||
dsmcfg = self._dsm_config(datasources=[])
|
dsmcfg = self._dsm_config(datasources=[])
|
||||||
manager = self._dsm(config=dsmcfg)
|
manager = self._dsm(config=dsmcfg)
|
||||||
|
|||||||
Reference in New Issue
Block a user