Allow using file to override metric map

Override the metric map of each datasource as soon as it is created by
the manager. This override comes from a file whose path is provided by
a setting in config file.

Loading at creation time allows the correct datasource be used when
get_backend is called, this allows loading a datasource whose metric
names get updated outside the watcher's codebase.

The function 'load_metric_map' returns empty-dict in any error case.
Also in case the file is empty where safe_load is unable finds any
yaml documents, it will return None. [1]

Some minor refactoring in the test_manager file for readability and
added tests for file load and metric override.

1 - https://pyyaml.org/wiki/PyYAMLDocumentation

Change-Id: I1df16245f4c7dfd34066f3ab0553cd67154faa58
Implements: blueprint file-based-metric-map
This commit is contained in:
Sumit Jamgade 2019-05-06 11:31:00 +02:00
parent 64d841b3f2
commit b620081714
4 changed files with 102 additions and 19 deletions

View File

@ -0,0 +1,11 @@
---
features:
- |
Allow using file to override metric map. Override the metric map of
each datasource as soon as it is created by the manager. This override
comes from a file whose path is provided by a setting in config file.
The setting is `watcher_decision_engine/metric_map_path`. The file
contains a map per datasource whose keys are the metric names as
recognized by watcher and the value is the real name of the metric
in the datasource. This setting defaults to `/etc/watcher/metric_map.yaml`,
and presence of this file is optional.

View File

@ -55,8 +55,20 @@ WATCHER_DECISION_ENGINE_OPTS = [
cfg.IntOpt('check_periodic_interval',
default=30 * 60,
mutable=True,
help='Interval (in seconds) for checking action plan expiry.')
]
help='Interval (in seconds) for checking action plan expiry.'),
cfg.StrOpt('metric_map_path',
default='/etc/watcher/metric_map.yaml',
help='Path to metric map yaml formatted file. '
' '
'The file contains a map per datasource whose keys '
'are the metric names as recognized by watcher and the '
'value is the real name of the metric in the datasource. '
'For example:: \n\n'
' monasca:\n'
' instance_cpu_usage: VM_CPU\n'
' gnocchi:\n'
' instance_cpu_usage: cpu_vm_util\n\n'
'This file is optional.')]
WATCHER_CONTINUOUS_OPTS = [
cfg.IntOpt('continuous_audit_interval',

View File

@ -13,13 +13,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import yaml
from collections import OrderedDict
from oslo_config import cfg
from oslo_log import log
from watcher.common import exception
from watcher.datasources import ceilometer as ceil
from watcher.datasources import gnocchi as gnoc
from watcher.datasources import monasca as mon
LOG = log.getLogger(__name__)
class DataSourceManager(object):
@ -38,6 +45,15 @@ class DataSourceManager(object):
self._ceilometer = None
self._monasca = None
self._gnocchi = None
metric_map_path = cfg.CONF.watcher_decision_engine.metric_map_path
metrics_from_file = self.load_metric_map(metric_map_path)
for ds, mp in self.metric_map.items():
try:
self.metric_map[ds].update(metrics_from_file.get(ds, {}))
except KeyError:
msgargs = (ds, self.metric_map.keys())
LOG.warning('Invalid Datasource: %s. Allowed: %s ', *msgargs)
self.datasources = self.config.datasources
@property
@ -82,7 +98,23 @@ class DataSourceManager(object):
# Try to use a specific datasource but attempt additional
# datasources upon exceptions (if config has more datasources)
try:
return getattr(self, datasource)
ds = getattr(self, datasource)
ds.METRIC_MAP.update(self.metric_map[ds.NAME])
return ds
except Exception:
pass
raise exception.NoSuchMetric()
def load_metric_map(self, file_path):
"""Load metrics from the metric_map_path"""
if file_path and os.path.exists(file_path):
with open(file_path, 'r') as f:
try:
ret = yaml.safe_load(f.read())
# return {} if the file is empty
return ret if ret else {}
except yaml.YAMLError as e:
LOG.warning('Could not load %s: %s', file_path, e)
return {}
else:
return {}

View File

@ -16,44 +16,72 @@
import mock
from mock import MagicMock
from watcher.common import exception
from watcher.datasources import gnocchi
from watcher.datasources import manager as ds_manager
from watcher.datasources import monasca
from watcher.tests import base
class TestDataSourceManager(base.BaseTestCase):
def _dsm_config(self, **kwargs):
dss = ['gnocchi', 'ceilometer', 'monasca']
opts = dict(datasources=dss, metric_map_path=None)
opts.update(kwargs)
return MagicMock(**opts)
def _dsm(self, **kwargs):
opts = dict(config=self._dsm_config(), osc=mock.MagicMock())
opts.update(kwargs)
return ds_manager.DataSourceManager(**opts)
def test_get_backend(self):
manager = ds_manager.DataSourceManager(
config=mock.MagicMock(
datasources=['gnocchi', 'ceilometer', 'monasca']),
osc=mock.MagicMock())
manager = self._dsm()
backend = manager.get_backend(['host_cpu_usage', 'instance_cpu_usage'])
self.assertEqual(backend, manager.gnocchi)
def test_get_backend_order(self):
manager = ds_manager.DataSourceManager(
config=mock.MagicMock(
datasources=['monasca', 'ceilometer', 'gnocchi']),
osc=mock.MagicMock())
dss = ['monasca', 'ceilometer', 'gnocchi']
dsmcfg = self._dsm_config(datasources=dss)
manager = self._dsm(config=dsmcfg)
backend = manager.get_backend(['host_cpu_usage', 'instance_cpu_usage'])
self.assertEqual(backend, manager.monasca)
def test_get_backend_wrong_metric(self):
manager = ds_manager.DataSourceManager(
config=mock.MagicMock(
datasources=['gnocchi', 'ceilometer', 'monasca']),
osc=mock.MagicMock())
manager = self._dsm()
self.assertRaises(exception.NoSuchMetric, manager.get_backend,
['host_cpu', 'instance_cpu_usage'])
@mock.patch.object(gnocchi, 'GnocchiHelper')
def test_get_backend_error_datasource(self, m_gnocchi):
m_gnocchi.side_effect = exception.DataSourceNotAvailable
manager = ds_manager.DataSourceManager(
config=mock.MagicMock(
datasources=['gnocchi', 'ceilometer', 'monasca']),
osc=mock.MagicMock())
manager = self._dsm()
backend = manager.get_backend(['host_cpu_usage', 'instance_cpu_usage'])
self.assertEqual(backend, manager.ceilometer)
def test_metric_file_path_not_exists(self):
manager = self._dsm()
expected = ds_manager.DataSourceManager.metric_map
actual = manager.metric_map
self.assertEqual(expected, actual)
self.assertEqual({}, manager.load_metric_map('/nope/nope/nope.yaml'))
def test_metric_file_metric_override(self):
path = 'watcher.datasources.manager.DataSourceManager.load_metric_map'
retval = {
monasca.MonascaHelper.NAME: {"host_airflow": "host_fnspid"}
}
with mock.patch(path, return_value=retval):
dsmcfg = self._dsm_config(datasources=['monasca'])
manager = self._dsm(config=dsmcfg)
backend = manager.get_backend(['host_cpu_usage'])
self.assertEqual("host_fnspid", backend.METRIC_MAP['host_airflow'])
def test_metric_file_invalid_ds(self):
with mock.patch('yaml.safe_load') as mo:
mo.return_value = {"newds": {"metric_one": "i_am_metric_one"}}
mgr = self._dsm()
self.assertNotIn('newds', mgr.metric_map.keys())