f903de3f10
Fix logging of debug, info and warning levels. Also configuration parameter VERBOSE is working again. fixes problems: * VERBOSE parameter can be changed at runtime * Initial log level was unset, then there was nothing logged lower than exception level. (for example configuration dump was not logged) * Initial VERBOSE was set to True, then it was too much verbose. And this flag was mean to be used for debugging problems. NOTE: real log level is driven by collectd configuration, not by plugin config. Change-Id: Ia7048ccb74f27a5d5885b9c0bda17d6fba603e9b Closes-Bug: #1664973
561 lines
20 KiB
Python
561 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright 2010-2011 OpenStack Foundation
|
|
# Copyright (c) 2015 Intel Corporation.
|
|
#
|
|
# 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.
|
|
|
|
"""Plugin tests."""
|
|
|
|
import abc
|
|
import logging
|
|
import mock
|
|
import requests
|
|
import six
|
|
import unittest
|
|
|
|
from collectd_ceilometer.aodh import plugin
|
|
from collectd_ceilometer.aodh import sender as aodh_sender
|
|
from collectd_ceilometer.common.keystone_light import KeystoneException
|
|
from collectd_ceilometer.common.meters import base
|
|
from collectd_ceilometer.common import sender as common_sender
|
|
from collectd_ceilometer.common import settings
|
|
|
|
Logger = logging.getLoggerClass()
|
|
|
|
|
|
def mock_collectd(**kwargs):
|
|
"""Return collecd module with collecd logging hooks."""
|
|
return mock.patch(
|
|
__name__ + '.' + MockedCollectd.__name__, specs=True,
|
|
get_dataset=mock.MagicMock(side_effect=Exception),
|
|
get=mock.MagicMock(), **kwargs)
|
|
|
|
|
|
class MockedCollectd(object):
|
|
"""Mocked collectd module specifications."""
|
|
|
|
def debug(self, record):
|
|
"""Hook for debug messages."""
|
|
|
|
def info(self, record):
|
|
"""Hook for info messages."""
|
|
|
|
def warning(self, record):
|
|
"""Hook for warning messages."""
|
|
|
|
def error(self, record):
|
|
"""Hook for error messages."""
|
|
|
|
def register_init(self, hook):
|
|
"""Register an hook for init."""
|
|
|
|
def register_config(self, hook):
|
|
"""Register an hook for config."""
|
|
|
|
def register_notification(self, hook):
|
|
"""Register an hook for notification."""
|
|
|
|
def register_shutdown(self, hook):
|
|
"""Register an hook for shutdown."""
|
|
|
|
def get_dataset(self, s):
|
|
"""Get a dataset."""
|
|
|
|
def get(self):
|
|
"""Get notification severity."""
|
|
|
|
|
|
def mock_config(**kwargs):
|
|
"""Return collecd module with collecd logging hooks."""
|
|
return mock.patch(
|
|
__name__ + '.' + MockedConfig.__name__, specs=True,
|
|
**kwargs)
|
|
|
|
|
|
def config_module(
|
|
values, severities=None,
|
|
module_name="collectd_ceilometer.ceilometer.plugin"):
|
|
children = [config_value(key, value)
|
|
for key, value in six.iteritems(values)]
|
|
if severities:
|
|
children.append(config_severities(severities))
|
|
return config_node('MODULE', children=children, value=module_name)
|
|
|
|
|
|
def config_severities(severities):
|
|
children = [config_value('ALARM_SEVERITY', key, value)
|
|
for key, value in six.iteritems(severities)]
|
|
return config_node('ALARM_SEVERITIES', children)
|
|
|
|
|
|
def config_node(key, children, value=None):
|
|
"Create a mocked collectd config node having given children and value"
|
|
return mock.create_autospec(
|
|
spec=MockedConfig, spec_set=True, instance=True,
|
|
children=tuple(children), key=key, values=(value,))
|
|
|
|
|
|
def config_value(key, *values):
|
|
"Create a mocked collectd config node having given multiple values"
|
|
return mock.create_autospec(
|
|
spec=MockedConfig, spec_set=True, instance=True,
|
|
children=tuple(), key=key, values=values)
|
|
|
|
|
|
class MockedConfig(object):
|
|
"""Mocked config class."""
|
|
|
|
@abc.abstractproperty
|
|
def children(self):
|
|
pass
|
|
|
|
@abc.abstractproperty
|
|
def key(self):
|
|
pass
|
|
|
|
@abc.abstractproperty
|
|
def values(self):
|
|
pass
|
|
|
|
|
|
def mock_value(
|
|
host='localhost', plugin='cpu', plugin_instance='0',
|
|
_type='freq', type_instance=None, time=123456789,
|
|
values=(1234,), **kwargs):
|
|
"""Create a mock value."""
|
|
return mock.patch(
|
|
__name__ + '.' + MockedValue.__name__, specs=True,
|
|
host=host, plugin=plugin, plugin_instance=plugin_instance, type=_type,
|
|
type_instance=type_instance, time=time,
|
|
values=list(values), meta=None, **kwargs)
|
|
|
|
|
|
class MockedValue(object):
|
|
"""Value used for testing."""
|
|
|
|
host = 'localhost'
|
|
plugin = None
|
|
plugin_instance = None
|
|
type = None
|
|
type_instance = None
|
|
time = 123456789
|
|
values = []
|
|
meta = None
|
|
|
|
|
|
class TestPlugin(unittest.TestCase):
|
|
"""Test the collectd plugin."""
|
|
|
|
@property
|
|
def default_values(self):
|
|
return dict(
|
|
BATCH_SIZE=1,
|
|
OS_AUTH_URL='https://test-auth.url.tld/test',
|
|
CEILOMETER_URL_TYPE='internalURL',
|
|
CEILOMETER_TIMEOUT=1000,
|
|
OS_USERNAME='tester',
|
|
OS_PASSWORD='testpasswd',
|
|
OS_TENANT_NAME='service')
|
|
|
|
@mock.patch.object(plugin, 'Plugin', autospec=True)
|
|
@mock.patch.object(plugin, 'Config', autospec=True)
|
|
@mock.patch.object(plugin, 'CollectdLogHandler', autospec=True)
|
|
@mock.patch.object(plugin, 'ROOT_LOGGER', autospec=True)
|
|
@mock_collectd()
|
|
def test_callbacks(
|
|
self, collectd, ROOT_LOGGER, CollectdLogHandler, Config, Plugin):
|
|
"""Verify that the callbacks are registered properly."""
|
|
# When plugin function is called
|
|
plugin.register_plugin(collectd=collectd)
|
|
|
|
# Logger handler is set up
|
|
ROOT_LOGGER.addHandler.assert_called_once_with(
|
|
CollectdLogHandler.return_value)
|
|
ROOT_LOGGER.setLevel.assert_called_once_with(logging.DEBUG)
|
|
|
|
# It creates a plugin
|
|
Plugin.assert_called_once_with(
|
|
collectd=collectd, config=Config.instance.return_value)
|
|
|
|
# callbacks are registered to collectd
|
|
instance = Plugin.return_value
|
|
collectd.register_config.assert_called_once_with(instance.config)
|
|
collectd.register_notification.assert_called_once_with(instance.notify)
|
|
collectd.register_shutdown.assert_called_once_with(instance.shutdown)
|
|
|
|
@mock.patch.object(aodh_sender.Sender, '_get_alarm_id', autospec=True)
|
|
@mock.patch.object(aodh_sender.Sender, '_get_alarm_state', autospec=True)
|
|
@mock.patch.object(requests, 'put', spec=callable)
|
|
@mock.patch.object(common_sender, 'ClientV3', autospec=True)
|
|
@mock_collectd()
|
|
@mock_config()
|
|
@mock_value()
|
|
def test_update_alarm(self, data, config, collectd, ClientV3,
|
|
put, _get_alarm_state, _get_alarm_id):
|
|
"""Test the update alarm function.
|
|
|
|
Set-up: get an alarm-id for some notification values to be sent
|
|
Test: perform an update request
|
|
Expected behaviour:
|
|
- If alarm-id is present a put request is performed
|
|
"""
|
|
auth_client = ClientV3.return_value
|
|
auth_client.get_service_endpoint.return_value = \
|
|
'https://test-aodh.tld'
|
|
|
|
# init instance
|
|
instance = plugin.Plugin(collectd=collectd, config=config)
|
|
|
|
# init values to send
|
|
_get_alarm_id.return_value = 'my-alarm-id'
|
|
_get_alarm_state.return_value = 'insufficient data'
|
|
|
|
# notify aodh of the update
|
|
instance.notify(data)
|
|
|
|
# update the alarm with a put request
|
|
put.assert_called_once_with(
|
|
'https://test-aodh.tld' +
|
|
'/v2/alarms/my-alarm-id/state',
|
|
data='"insufficient data"',
|
|
headers={'Content-type': 'application/json',
|
|
'X-Auth-Token': auth_client.auth_token},
|
|
timeout=1.0)
|
|
|
|
# reset method
|
|
put.reset_mock()
|
|
|
|
@mock.patch.object(aodh_sender.Sender, '_create_alarm', autospec=True)
|
|
@mock.patch.object(aodh_sender.Sender, '_get_alarm_id', autospec=True)
|
|
@mock.patch.object(requests, 'put', spec=callable)
|
|
@mock.patch.object(common_sender, 'ClientV3', autospec=True)
|
|
@mock_collectd()
|
|
@mock_config()
|
|
@mock_value()
|
|
def test_update_alarm_no_id(self, data, config, collectd, ClientV3,
|
|
put, _get_alarm_id, _create_alarm):
|
|
"""Test if the is no alarm id the alarm won't be updated.
|
|
|
|
Set-up: create a client and an instance to send an update to
|
|
throw a side-effect when looking for an id
|
|
Test: send a notification for a new alarm
|
|
Expected behaviour:
|
|
- if an alarm is create an update request is not performed
|
|
"""
|
|
auth_client = ClientV3.return_value
|
|
auth_client.get_service_endpoint.return_value = \
|
|
'https://test-aodh.tld'
|
|
|
|
instance = plugin.Plugin(collectd=collectd, config=config)
|
|
|
|
# init values to send
|
|
_get_alarm_id.return_value = None
|
|
_create_alarm.return_value = 'my-alarm-id'
|
|
|
|
# try and perform an update without an id
|
|
instance.notify(data)
|
|
|
|
put.assert_not_called()
|
|
|
|
put.reset_mock()
|
|
|
|
@mock.patch.object(requests, 'put', spec=callable)
|
|
@mock.patch.object(common_sender, 'ClientV3', autospec=True)
|
|
@mock.patch.object(common_sender, 'LOGGER', autospec=True)
|
|
@mock_collectd()
|
|
@mock_config()
|
|
@mock_value()
|
|
def test_notify_auth_failed(
|
|
self, data, config, collectd, LOGGER, ClientV3, put):
|
|
"""Test authentication failure."""
|
|
# tell the auth client to raise an exception
|
|
ClientV3.side_effect = KeystoneException(
|
|
"Missing name 'xxx' in received services",
|
|
"exception",
|
|
"services list")
|
|
|
|
# init instance
|
|
instance = plugin.Plugin(collectd=collectd, config=config)
|
|
|
|
# notify of another value the value
|
|
instance.notify(data)
|
|
|
|
LOGGER.error.assert_called_once_with(
|
|
"Suspending error logs until successful auth")
|
|
LOGGER.log.assert_called_once_with(
|
|
logging.ERROR, "Authentication error: %s",
|
|
"Missing name 'xxx' in received services\nReason: exception",
|
|
exc_info=0)
|
|
|
|
# no requests method has been called
|
|
put.assert_not_called()
|
|
|
|
@mock.patch.object(base.Meter, 'collectd_severity', spec=callable)
|
|
def test_get_alarm_state_severity_low(self, severity):
|
|
"""Test _get_alarm_state if severity is 'low'.
|
|
|
|
Set-up: create a sender instance, set severity to low
|
|
Test: call _get_alarm_state method with severity=low
|
|
Expected-behaviour: returned state value should equal 'ok'
|
|
and won't equal 'alarm' or insufficient data'
|
|
"""
|
|
instance = aodh_sender.Sender()
|
|
|
|
# run test for moderate severity
|
|
severity.return_value = 'low'
|
|
|
|
self.assertEqual(instance._get_alarm_state('low'), 'ok')
|
|
|
|
self.assertNotEqual(instance._get_alarm_state('low'), 'alarm')
|
|
|
|
self.assertNotEqual(instance._get_alarm_state('low'),
|
|
'insufficient data')
|
|
|
|
@mock.patch.object(base.Meter, 'collectd_severity', spec=callable)
|
|
def test_get_alarm_state_severity_moderate(self, severity):
|
|
"""Test _get_alarm_state if severity is 'moderate'.
|
|
|
|
Set-up: create a sender instance, set severity to moderate
|
|
Test: call _get_alarm_state method with severity=moderate
|
|
Expected-behaviour: returned state value should equal 'alarm'
|
|
and won't equal 'ok' or insufficient data'
|
|
"""
|
|
instance = aodh_sender.Sender()
|
|
|
|
# run test for moderate severity
|
|
severity.return_value = 'moderate'
|
|
|
|
self.assertEqual(instance._get_alarm_state('moderate'), 'alarm')
|
|
|
|
self.assertNotEqual(instance._get_alarm_state('moderate'), 'ok')
|
|
|
|
self.assertNotEqual(instance._get_alarm_state('moderate'),
|
|
'insufficient data')
|
|
|
|
@mock.patch.object(base.Meter, 'collectd_severity', spec=callable)
|
|
def test_get_alarm_state_severity_critical(self, severity):
|
|
"""Test _get_alarm_state if severity is 'critical'.
|
|
|
|
Set-up: create a sender instance, set severity to critical
|
|
Test: call _get_alarm_state method with severity=critical
|
|
Expected-behaviour: returned state value should equal 'alarm'
|
|
and won't equal 'ok' or 'insufficient data'
|
|
"""
|
|
instance = aodh_sender.Sender()
|
|
|
|
# run test for moderate severity
|
|
severity.return_value = 'critical'
|
|
|
|
self.assertEqual(instance._get_alarm_state('critical'), 'alarm')
|
|
|
|
self.assertNotEqual(instance._get_alarm_state('critical'), 'ok')
|
|
|
|
self.assertNotEqual(instance._get_alarm_state('critical'),
|
|
'insufficient data')
|
|
|
|
@mock.patch.object(common_sender.Sender, '_perform_request', spec=callable)
|
|
@mock.patch.object(common_sender, 'ClientV3', autospec=True)
|
|
@mock_collectd()
|
|
@mock_config()
|
|
@mock_value()
|
|
def test_request_error(
|
|
self, data, config, collectd, ClientV3, perf_req):
|
|
"""Test error raised by underlying requests module."""
|
|
# tell POST request to raise an exception
|
|
perf_req.side_effect = requests.RequestException('Test POST exception')
|
|
|
|
# init instance
|
|
instance = plugin.Plugin(collectd=collectd, config=config)
|
|
|
|
# the value
|
|
self.assertRaises(requests.RequestException, instance.notify, data)
|
|
|
|
@mock.patch.object(aodh_sender.Sender, '_get_alarm_state', autospec=True)
|
|
@mock.patch.object(aodh_sender.Sender, '_get_alarm_id', autospec=True)
|
|
@mock.patch.object(requests, 'put', spec=callable)
|
|
@mock.patch.object(common_sender, 'ClientV3', autospec=True)
|
|
@mock_collectd()
|
|
@mock_config()
|
|
@mock_value()
|
|
def test_reauthentication(self, data, config, collectd,
|
|
ClientV3, put, _get_alarm_id, _get_alarm_state):
|
|
"""Test re-authentication for update request."""
|
|
|
|
# response returned on success
|
|
response_ok = requests.Response()
|
|
response_ok.status_code = requests.codes["OK"]
|
|
|
|
# response returned on failure
|
|
response_unauthorized = requests.Response()
|
|
response_unauthorized.status_code = requests.codes["UNAUTHORIZED"]
|
|
|
|
# set-up client
|
|
client = ClientV3.return_value
|
|
client.auth_token = 'Test auth token'
|
|
client.get_service_endpoint.return_value = \
|
|
'https://test-aodh.tld'
|
|
|
|
# init instance attempt to update/create alarm
|
|
instance = plugin.Plugin(collectd=collectd, config=config)
|
|
|
|
put.return_value = response_ok
|
|
_get_alarm_id.return_value = 'my-alarm-id'
|
|
_get_alarm_state.return_value = 'insufficient data'
|
|
|
|
# send notification to aodh
|
|
instance.notify(data)
|
|
|
|
# put/update is called
|
|
put.assert_called_once_with(
|
|
'https://test-aodh.tld' +
|
|
'/v2/alarms/my-alarm-id/state',
|
|
data='"insufficient data"',
|
|
headers={u'Content-type': 'application/json',
|
|
u'X-Auth-Token': 'Test auth token'},
|
|
timeout=1.0)
|
|
|
|
@mock.patch.object(common_sender, 'ClientV3', autospec=True)
|
|
@mock.patch.object(plugin, 'Notifier', autospec=True)
|
|
@mock.patch.object(plugin, 'LOGGER', autospec=True)
|
|
@mock_collectd()
|
|
@mock_config()
|
|
@mock_value()
|
|
def test_exception_value_error(self, data, config, collectd,
|
|
LOGGER, Notifier, ClientV3):
|
|
"""Test exception raised during notify and shutdown."""
|
|
notifier = Notifier.return_value
|
|
notifier.notify.side_effect = ValueError('Test notify error')
|
|
|
|
# init instance
|
|
instance = plugin.Plugin(collectd=collectd, config=config)
|
|
|
|
self.assertRaises(ValueError, instance.notify, data)
|
|
|
|
@mock.patch.object(common_sender, 'ClientV3', autospec=True)
|
|
@mock.patch.object(plugin, 'LOGGER', autospec=True)
|
|
@mock_collectd()
|
|
@mock_config()
|
|
def test_exception_runtime_error(self, config, collectd,
|
|
LOGGER, ClientV3):
|
|
"""Test exception raised during shutdown."""
|
|
# init instance
|
|
instance = plugin.Plugin(collectd=collectd, config=config)
|
|
|
|
instance.shutdown
|
|
|
|
@mock.patch.object(settings, 'LOGGER', autospec=True)
|
|
def test_user_severities(self, LOGGER):
|
|
"""Test if a user enters a severity for a specific meter
|
|
|
|
Set-up: Create a node with some user defined severities
|
|
Configure the node
|
|
Test: Read the configured node and compare the results
|
|
of the method to the severities configured in the node
|
|
Expected-behaviour: Valid mapping metric names are mapped correctly
|
|
to severities, and invalid values return None.
|
|
"""
|
|
node = config_module(
|
|
values=self.default_values,
|
|
severities={'age': 'low',
|
|
'star.distance': 'moderate',
|
|
'star.temperature': 'critical'})
|
|
config = settings.Config._decorated()
|
|
|
|
config.read(node)
|
|
|
|
LOGGER.error.assert_not_called()
|
|
self.assertEqual(config.alarm_severity('age'), 'low')
|
|
self.assertEqual(config.alarm_severity('star.distance'), 'moderate')
|
|
self.assertEqual(config.alarm_severity('star.temperature'), 'critical')
|
|
self.assertEqual(config.alarm_severity('monty'), 'moderate')
|
|
self.assertEqual(config.alarm_severity('python'), 'moderate')
|
|
|
|
@mock.patch.object(settings, 'LOGGER', autospec=True)
|
|
def test_user_severities_invalid(self, LOGGER):
|
|
"""Test invalid user defined severities
|
|
|
|
Set-up: Configure the node with one defined severity
|
|
Set a configuration to have 3 entries instead of the 2
|
|
which are expected
|
|
Test: Try to read the configuration node with incorrect configurations
|
|
Compare the configuration to the response on the method
|
|
Expected-behaviour: alarm_severity will return None
|
|
Log will be written that severities were
|
|
incorrectly configured
|
|
"""
|
|
|
|
node = config_module(values=self.default_values,
|
|
severities=dict(age='low'))
|
|
# make some alarm severity entry invalid
|
|
for child in node.children:
|
|
if child.key == 'ALARM_SEVERITIES':
|
|
child.children[0].values = (1, 2, 3)
|
|
break
|
|
config = settings.Config._decorated()
|
|
|
|
config.read(node)
|
|
|
|
self.assertEqual(config.alarm_severity('age'), 'moderate')
|
|
LOGGER.error.assert_called_with(
|
|
'Invalid alarm severity configuration: \
|
|
severity "1" "2" "3"')
|
|
|
|
@mock.patch.object(settings, 'LOGGER', autospec=True)
|
|
def test_user_severities_invalid_node(self, LOGGER):
|
|
"""Test invalid node with severities configuration
|
|
|
|
Set-up: Set up a configuration node with a severity defined
|
|
Configure the node with an incorrect module title
|
|
Test: Read the incorrect configuration node
|
|
Expected-behaviour: Error will be recorded in the log
|
|
Severity configuration will return None
|
|
"""
|
|
|
|
node = config_module(values=self.default_values,
|
|
severities=dict(age='moderate'))
|
|
# make some alarm severity entry invalid
|
|
for child in node.children:
|
|
if child.key == 'ALARM_SEVERITIES':
|
|
child.children[0].key = 'NOT_SEVERITIES'
|
|
break
|
|
config = settings.Config._decorated()
|
|
|
|
config.read(node)
|
|
|
|
LOGGER.error.assert_called_with(
|
|
'Invalid alarm severity configuration: %s', "NOT_SEVERITIES")
|
|
self.assertEqual(config.alarm_severity('age'), 'moderate')
|
|
|
|
def test_read_alarm_severities(self):
|
|
"""Test reading in user defined alarm severities method
|
|
|
|
Set-up: Set up a node configured with a severities dictionary defined
|
|
Test: Read the node for the ALARM_SEVERITY configuration
|
|
Expected-behaviour: Info log will be recorded
|
|
Severities are correctly configured
|
|
"""
|
|
node = config_module(values=self.default_values,
|
|
severities=dict(age='low'))
|
|
|
|
for n in node.children:
|
|
if n.key.upper() == 'ALARM_SEVERITY':
|
|
if len(n.values) == 2:
|
|
key, val = n.values
|
|
break
|
|
config = settings.Config._decorated()
|
|
|
|
config._read_node(node)
|
|
|
|
self.assertEqual('low', config.alarm_severity('age'))
|