Refactor and test main entry point.
plugin.py is really hard to test because good part of its logic happend during code importation. It's test depends on the base TestCase that mocks too much thinks in a way that makes hard to spot real problems. The test also fails to test re-authenthication bug because of bad mocking. This code should fix above problems. It also remove some logic in the hooks: exceptions are already captured and logged by collectd and there is no need doing it inside of hooks. Init hook is also pointless from the perspective of this plugin initialization and it was removed. Co-authored-by: Emma Foley <emma.l.foley@intel.com> Related-Bug: #1615349 Change-Id: I4db8a94243ecbe98cd6bc13e4b66293172346dcd
This commit is contained in:
parent
0d03fa8139
commit
9cf7d56f5a
@ -13,81 +13,72 @@
|
||||
# under the License.
|
||||
"""Ceilometer collectd plugin"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
|
||||
# pylint: disable=import-error
|
||||
import collectd
|
||||
# pylint: enable=import-error
|
||||
try:
|
||||
# pylint: disable=import-error
|
||||
import collectd
|
||||
# pylint: enable=import-error
|
||||
except ImportError:
|
||||
collectd = None # when running unit tests collectd is not avaliable
|
||||
|
||||
import collectd_ceilometer
|
||||
from collectd_ceilometer.logger import CollectdLogHandler
|
||||
from collectd_ceilometer.meters import MeterStorage
|
||||
from collectd_ceilometer.settings import Config
|
||||
from collectd_ceilometer.writer import Writer
|
||||
import logging
|
||||
|
||||
|
||||
log_handler = CollectdLogHandler(collectd=collectd)
|
||||
logging.getLogger().addHandler(log_handler)
|
||||
logging.getLogger().setLevel(logging.NOTSET)
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
ROOT_LOGGER = logging.getLogger(collectd_ceilometer.__name__)
|
||||
|
||||
|
||||
def register_plugin(collectd):
|
||||
"Bind plugin hooks to collectd and viceversa"
|
||||
|
||||
config = Config.instance()
|
||||
|
||||
# Setup loggging
|
||||
log_handler = CollectdLogHandler(collectd=collectd)
|
||||
log_handler.cfg = config
|
||||
ROOT_LOGGER.addHandler(log_handler)
|
||||
ROOT_LOGGER.setLevel(logging.NOTSET)
|
||||
|
||||
# Creates collectd plugin instance
|
||||
instance = Plugin(collectd=collectd, config=config)
|
||||
|
||||
# Register plugin callbacks
|
||||
collectd.register_config(instance.config)
|
||||
collectd.register_write(instance.write)
|
||||
collectd.register_shutdown(instance.shutdown)
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
"""Ceilometer plugin with collectd callbacks"""
|
||||
# NOTE: this is multithreaded class
|
||||
|
||||
def __init__(self):
|
||||
self._meters = None
|
||||
self._writer = None
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
def __init__(self, collectd, config):
|
||||
self._config = config
|
||||
self._meters = MeterStorage(collectd=collectd)
|
||||
self._writer = Writer(self._meters, config=config)
|
||||
|
||||
def config(self, cfg):
|
||||
"""Configuration callback
|
||||
|
||||
@param cfg configuration node provided by collectd
|
||||
"""
|
||||
# pylint: disable=no-self-use
|
||||
config = Config.instance()
|
||||
config.read(cfg)
|
||||
|
||||
# apply configuration
|
||||
log_handler.verbose = config.VERBOSE
|
||||
|
||||
def init(self):
|
||||
"""Initialization callback"""
|
||||
|
||||
collectd.info('Initializing the collectd OpenStack python plugin')
|
||||
self._meters = MeterStorage(collectd=collectd)
|
||||
self._writer = Writer(self._meters)
|
||||
self._config.read(cfg)
|
||||
|
||||
def write(self, vl, data=None):
|
||||
"""Collectd write callback"""
|
||||
# pylint: disable=broad-except
|
||||
# pass arguments to the writer
|
||||
try:
|
||||
self._writer.write(vl, data)
|
||||
except Exception as exc:
|
||||
if collectd is not None:
|
||||
collectd.error('Exception during write: %s' % exc)
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown callback"""
|
||||
# pylint: disable=broad-except
|
||||
collectd.info("SHUTDOWN")
|
||||
try:
|
||||
LOGGER.info("SHUTDOWN")
|
||||
self._writer.flush()
|
||||
except Exception as exc:
|
||||
if collectd is not None:
|
||||
collectd.error('Exception during shutdown: %s' % exc)
|
||||
|
||||
|
||||
# The collectd plugin instance
|
||||
# pylint: disable=invalid-name
|
||||
instance = Plugin()
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
# Register plugin callbacks
|
||||
collectd.register_init(instance.init)
|
||||
collectd.register_config(instance.config)
|
||||
collectd.register_write(instance.write)
|
||||
collectd.register_shutdown(instance.shutdown)
|
||||
if collectd:
|
||||
register_plugin(collectd=collectd)
|
||||
|
52
collectd_ceilometer/tests/match.py
Normal file
52
collectd_ceilometer/tests/match.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2016 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.
|
||||
|
||||
import fnmatch
|
||||
from json import loads
|
||||
import re
|
||||
|
||||
|
||||
def json(obj):
|
||||
return MatchJson(obj)
|
||||
|
||||
|
||||
class MatchJson(object):
|
||||
|
||||
def __init__(self, obj):
|
||||
self._obj = obj
|
||||
|
||||
def __eq__(self, json_text):
|
||||
return self._obj == loads(json_text)
|
||||
|
||||
def __repr__(self):
|
||||
return "MatchJson({})".format(repr(self._obj))
|
||||
|
||||
|
||||
def wildcard(text):
|
||||
return MatchWildcard(text)
|
||||
|
||||
|
||||
class MatchWildcard(object):
|
||||
|
||||
def __init__(self, obj):
|
||||
self._text = text = str(obj)
|
||||
self._reg = re.compile(fnmatch.translate(text))
|
||||
|
||||
def __eq__(self, obj):
|
||||
return self._reg.match(str(obj))
|
||||
|
||||
def __repr__(self):
|
||||
return "MatchWildcard({})".format(self._text)
|
@ -14,233 +14,309 @@
|
||||
# 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
|
||||
|
||||
"""Plugin tests"""
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collectd_ceilometer.tests.base import TestCase
|
||||
from collectd_ceilometer.tests.base import Value
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import mock
|
||||
import logging
|
||||
import requests
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from collectd_ceilometer import keystone_light
|
||||
from collectd_ceilometer import plugin
|
||||
from collectd_ceilometer import sender
|
||||
|
||||
from collectd_ceilometer.tests import match
|
||||
|
||||
|
||||
class PluginTest(TestCase):
|
||||
Logger = logging.getLoggerClass()
|
||||
|
||||
|
||||
def mock_collectd(**kwargs):
|
||||
"Returns collecd module with collecd logging hooks."
|
||||
return mock.patch(
|
||||
__name__ + '.' + MockedCollectd.__name__, specs=True,
|
||||
get_dataset=mock.MagicMock(side_effect=Exception), **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_write(self, hook):
|
||||
"Register an hook for write."
|
||||
|
||||
def register_shutdown(self, hook):
|
||||
"Register an hook for shutdown."
|
||||
|
||||
def get_dataset(self, s):
|
||||
"Gets a dataset."
|
||||
|
||||
|
||||
def mock_config(BATCH_SIZE=1, **kwargs):
|
||||
"Returns collecd module with collecd logging hooks."
|
||||
return mock.patch(
|
||||
__name__ + '.' + MockedConfig.__name__, specs=True,
|
||||
BATCH_SIZE=BATCH_SIZE, **kwargs)
|
||||
|
||||
|
||||
class MockedConfig(object):
|
||||
"Mocked config class."
|
||||
|
||||
BATCH_SIZE = 1
|
||||
OS_AUTH_URL = "http://test-url"
|
||||
|
||||
|
||||
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"""
|
||||
|
||||
def setUp(self):
|
||||
super(PluginTest, self).setUp()
|
||||
client_class \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
|
||||
client_class.return_value\
|
||||
.get_service_endpoint.return_value = "https://test-ceilometer.tld"
|
||||
|
||||
# TODO(emma-l-foley): Import at top and mock here
|
||||
from collectd_ceilometer.plugin import instance
|
||||
from collectd_ceilometer.plugin import Plugin
|
||||
self.default_instance = instance
|
||||
self.plugin_instance = Plugin()
|
||||
self.maxDiff = None
|
||||
|
||||
def test_callbacks(self):
|
||||
@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"""
|
||||
|
||||
collectd = self.get_mock('collectd')
|
||||
# When plugin function is called
|
||||
plugin.register_plugin(collectd=collectd)
|
||||
|
||||
self.assertTrue(collectd.register_init.called)
|
||||
self.assertTrue(collectd.register_config.called)
|
||||
self.assertTrue(collectd.register_write.called)
|
||||
self.assertTrue(collectd.register_shutdown.called)
|
||||
# Logger handler is set up
|
||||
ROOT_LOGGER.addHandler.assert_called_once_with(
|
||||
CollectdLogHandler.return_value)
|
||||
ROOT_LOGGER.setLevel.assert_called_once_with(logging.NOTSET)
|
||||
|
||||
# It create 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_write.assert_called_once_with(instance.write)
|
||||
collectd.register_shutdown.assert_called_once_with(instance.shutdown)
|
||||
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_write(self, post):
|
||||
@mock.patch.object(sender, 'ClientV3', autospec=True)
|
||||
@mock_collectd()
|
||||
@mock_config(BATCH_SIZE=2)
|
||||
@mock_value()
|
||||
def test_write(self, data, config, collectd, ClientV3, post):
|
||||
"""Test collectd data writing"""
|
||||
from collectd_ceilometer.sender import HTTP_CREATED
|
||||
|
||||
post.return_value = response = requests.Response()
|
||||
response.status_code = HTTP_CREATED
|
||||
auth_client = ClientV3.return_value
|
||||
auth_client.get_service_endpoint.return_value =\
|
||||
'https://test-ceilometer.tld'
|
||||
|
||||
client_class \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
|
||||
auth_token = client_class.return_value.auth_token
|
||||
post.return_value.status_code = sender.HTTP_CREATED
|
||||
post.return_value.text = 'Created'
|
||||
|
||||
# create a value
|
||||
data = self._create_value()
|
||||
|
||||
# set batch size to 2 and init instance
|
||||
self.config.update_value('BATCH_SIZE', 2)
|
||||
self._init_instance()
|
||||
# init instance
|
||||
instance = plugin.Plugin(collectd=collectd, config=config)
|
||||
|
||||
# no authentication has been performed so far
|
||||
self.assertFalse(client_class.called)
|
||||
ClientV3.assert_not_called()
|
||||
|
||||
# write first value
|
||||
self._write_value(data)
|
||||
instance.write(data)
|
||||
collectd.error.assert_not_called()
|
||||
|
||||
# no value has been sent to ceilometer
|
||||
post.assert_not_called()
|
||||
|
||||
# send the second value
|
||||
self._write_value(data)
|
||||
instance.write(data)
|
||||
collectd.error.assert_not_called()
|
||||
|
||||
# authentication client has been created
|
||||
self.assertTrue(client_class.called)
|
||||
self.assertEqual(client_class.call_count, 1)
|
||||
ClientV3.assert_called_once()
|
||||
|
||||
# and values has been sent
|
||||
post.assert_called_once()
|
||||
|
||||
expected_args = ('https://test-ceilometer.tld/v2/meters/cpu.freq',)
|
||||
expected_kwargs = {
|
||||
'data': [{
|
||||
"source": "collectd",
|
||||
post.assert_called_once_with(
|
||||
'https://test-ceilometer.tld/v2/meters/cpu.freq',
|
||||
data=match.json([
|
||||
{"source": "collectd",
|
||||
"counter_name": "cpu.freq",
|
||||
"counter_unit": "jiffies",
|
||||
"counter_volume": 1234,
|
||||
"timestamp": "Thu Nov 29 21:33:09 1973",
|
||||
"resource_id": "localhost-0",
|
||||
"resource_metadata": None,
|
||||
"counter_type": "gauge"
|
||||
}, {
|
||||
"source": "collectd",
|
||||
"counter_type": "gauge"},
|
||||
{"source": "collectd",
|
||||
"counter_name": "cpu.freq",
|
||||
"counter_unit": "jiffies",
|
||||
"counter_volume": 1234,
|
||||
"timestamp": "Thu Nov 29 21:33:09 1973",
|
||||
"resource_id": "localhost-0",
|
||||
"resource_metadata": None,
|
||||
"counter_type": "gauge"}],
|
||||
'headers': {
|
||||
'Content-type': u'application/json',
|
||||
'X-Auth-Token': auth_token},
|
||||
'timeout': 1.0}
|
||||
|
||||
# we cannot compare JSON directly because the original data
|
||||
# dictionary is unordered
|
||||
called_kwargs = post.call_args[1]
|
||||
called_kwargs['data'] = json.loads(called_kwargs['data'])
|
||||
|
||||
# verify data sent to ceilometer
|
||||
self.assertEqual(post.call_args[0], expected_args)
|
||||
self.assertEqual(called_kwargs, expected_kwargs)
|
||||
"counter_type": "gauge"}]),
|
||||
headers={'Content-type': 'application/json',
|
||||
'X-Auth-Token': auth_client.auth_token},
|
||||
timeout=1.0)
|
||||
|
||||
# reset post method
|
||||
post.reset_mock()
|
||||
|
||||
# write another values
|
||||
self._write_value(data)
|
||||
instance.write(data)
|
||||
collectd.error.assert_not_called()
|
||||
|
||||
# nothing has been sent
|
||||
post.assert_not_called()
|
||||
|
||||
# call shutdown
|
||||
self.plugin_instance.shutdown()
|
||||
self.assertNoError()
|
||||
# previously written value has been sent
|
||||
post.assert_called_once()
|
||||
# no more authentication required
|
||||
self.assertEqual(client_class.call_count, 1)
|
||||
instance.shutdown()
|
||||
|
||||
expected_kwargs = {
|
||||
'data': [{
|
||||
"source": "collectd",
|
||||
# no errors
|
||||
collectd.error.assert_not_called()
|
||||
|
||||
# previously written value has been sent
|
||||
post.assert_called_once_with(
|
||||
'https://test-ceilometer.tld/v2/meters/cpu.freq',
|
||||
data=match.json([
|
||||
{"source": "collectd",
|
||||
"counter_name": "cpu.freq",
|
||||
"counter_unit": "jiffies",
|
||||
"counter_volume": 1234,
|
||||
"timestamp": "Thu Nov 29 21:33:09 1973",
|
||||
"resource_id": "localhost-0",
|
||||
"resource_metadata": None,
|
||||
"counter_type": "gauge"}],
|
||||
'headers': {
|
||||
'Content-type': u'application/json',
|
||||
'X-Auth-Token': auth_token},
|
||||
'timeout': 1.0}
|
||||
|
||||
# we cannot compare JSON directly because the original data
|
||||
# dictionary is unordered
|
||||
called_kwargs = post.call_args[1]
|
||||
called_kwargs['data'] = json.loads(called_kwargs['data'])
|
||||
|
||||
# verify data sent to ceilometer
|
||||
self.assertEqual(post.call_args[0], expected_args)
|
||||
self.assertEqual(called_kwargs, expected_kwargs)
|
||||
"counter_type": "gauge"}]),
|
||||
headers={
|
||||
'Content-type': 'application/json',
|
||||
'X-Auth-Token': auth_client.auth_token},
|
||||
timeout=1.0)
|
||||
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_write_auth_failed(self, post):
|
||||
@mock.patch.object(sender, 'ClientV3', autospec=True)
|
||||
@mock.patch.object(plugin, 'LOGGER', autospec=True)
|
||||
@mock_collectd()
|
||||
@mock_config()
|
||||
@mock_value()
|
||||
def test_write_auth_failed(
|
||||
self, data, config, collectd, LOGGER, ClientV3, post):
|
||||
"""Test authentication failure"""
|
||||
|
||||
ClientV3.auth_url = "http://tst-url"
|
||||
# tell the auth client to rise an exception
|
||||
client_class \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
|
||||
client_class.side_effect = Exception('Test Client() exception')
|
||||
ClientV3.side_effect = RuntimeError('Test Client() exception')
|
||||
|
||||
# init instance
|
||||
self._init_instance()
|
||||
instance = plugin.Plugin(collectd=collectd, config=config)
|
||||
|
||||
# write the value
|
||||
errors = [
|
||||
'Exception during write: Test Client() exception']
|
||||
self._write_value(self._create_value(), errors)
|
||||
self.assertRaises(RuntimeError, instance.write, data)
|
||||
|
||||
# no requests method has been called
|
||||
post.assert_not_called()
|
||||
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_write_auth_failed2(self, post):
|
||||
@mock.patch.object(sender, 'ClientV3', autospec=True)
|
||||
@mock.patch.object(sender, 'LOGGER', autospec=True)
|
||||
@mock_collectd()
|
||||
@mock_config()
|
||||
@mock_value()
|
||||
def test_write_auth_failed2(
|
||||
self, data, config, collectd, LOGGER, ClientV3, post):
|
||||
"""Test authentication failure2"""
|
||||
|
||||
# tell the auth client to rise an exception
|
||||
keystone \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light')
|
||||
|
||||
client_class = keystone.ClientV2
|
||||
client_class.side_effect = keystone.KeystoneException(
|
||||
ClientV3.side_effect = keystone_light.KeystoneException(
|
||||
"Missing name 'xxx' in received services",
|
||||
"exception",
|
||||
"services list")
|
||||
|
||||
# init instance
|
||||
self._init_instance()
|
||||
instance = plugin.Plugin(collectd=collectd, config=config)
|
||||
|
||||
# write the value
|
||||
errors = [
|
||||
"Suspending error logs until successful auth",
|
||||
"Authentication error: Missing name 'xxx' in received services"
|
||||
"\nReason: exception"]
|
||||
self._write_value(self._create_value(), errors)
|
||||
instance.write(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
|
||||
post.assert_not_called()
|
||||
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_request_error(self, post):
|
||||
@mock.patch.object(sender, 'ClientV3', autospec=True)
|
||||
@mock_collectd()
|
||||
@mock_config()
|
||||
@mock_value()
|
||||
def test_request_error(
|
||||
self, data, config, collectd, ClientV3, post):
|
||||
"""Test error raised by underlying requests module"""
|
||||
|
||||
# we have to import the RequestException here as it has been mocked
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# tell POST request to raise an exception
|
||||
post.side_effect = RequestException('Test POST exception')
|
||||
post.side_effect = requests.RequestException('Test POST exception')
|
||||
|
||||
# init instance
|
||||
self._init_instance()
|
||||
instance = plugin.Plugin(collectd=collectd, config=config)
|
||||
|
||||
# write the value
|
||||
self._write_value(
|
||||
self._create_value(),
|
||||
['Exception during write: Test POST exception'])
|
||||
self.assertRaises(requests.RequestException, instance.write, data)
|
||||
|
||||
@mock.patch.object(sender.Sender, '_perform_request', spec=callable)
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_reauthentication(self, post):
|
||||
@mock.patch.object(sender, 'ClientV3', autospec=True)
|
||||
@mock_collectd()
|
||||
@mock_config()
|
||||
@mock_value()
|
||||
def test_reauthentication(self, data, config, collectd,
|
||||
ClientV3, post, perf_req):
|
||||
"""Test re-authentication"""
|
||||
|
||||
client_class \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
|
||||
client_class.return_value.auth_token = 'Test auth token'
|
||||
|
||||
# init instance
|
||||
self._init_instance()
|
||||
|
||||
# response returned on success
|
||||
response_ok = requests.Response()
|
||||
response_ok.status_code = requests.codes["OK"]
|
||||
@ -251,35 +327,48 @@ class PluginTest(TestCase):
|
||||
|
||||
# write the first value with success
|
||||
# subsequent call of POST method will fail due to the authentication
|
||||
post.side_effect = [response_ok, response_unauthorized, response_ok]
|
||||
self._write_value(self._create_value())
|
||||
perf_req.return_value = response_ok
|
||||
|
||||
client = ClientV3.return_value
|
||||
client.auth_url = "http://tst-url"
|
||||
client.auth_token = 'Test auth token'
|
||||
|
||||
# init instance
|
||||
instance = plugin.Plugin(collectd=collectd, config=config)
|
||||
|
||||
# write the value
|
||||
instance.write(data)
|
||||
|
||||
# verify the auth token
|
||||
call_list = post.call_args_list
|
||||
self.assertEqual(len(call_list), 1)
|
||||
# 0 = first call > 1 = call kwargs > headers argument > auth token
|
||||
token = call_list[0][1]['headers']['X-Auth-Token']
|
||||
self.assertEqual(token, 'Test auth token')
|
||||
perf_req.assert_called_once_with(
|
||||
mock.ANY, mock.ANY,
|
||||
'Test auth token')
|
||||
|
||||
# set a new auth token
|
||||
client_class.return_value.auth_token = 'New test auth token'
|
||||
client.auth_token = 'New test auth token'
|
||||
perf_req.side_effect = \
|
||||
[requests.exceptions.HTTPError(response=response_unauthorized),
|
||||
response_ok]
|
||||
|
||||
self._write_value(self._create_value())
|
||||
# write the value
|
||||
instance.write(data)
|
||||
|
||||
# verify the auth token
|
||||
call_list = post.call_args_list
|
||||
|
||||
# POST called three times
|
||||
self.assertEqual(len(call_list), 3)
|
||||
# the second call contains the old token
|
||||
token = call_list[1][1]['headers']['X-Auth-Token']
|
||||
self.assertEqual(token, 'Test auth token')
|
||||
# the third call contains the new token
|
||||
token = call_list[2][1]['headers']['X-Auth-Token']
|
||||
self.assertEqual(token, 'New test auth token')
|
||||
perf_req.assert_has_calls([
|
||||
mock.call(mock.ANY, mock.ANY,
|
||||
'Test auth token'),
|
||||
mock.call(mock.ANY, mock.ANY,
|
||||
'New test auth token')
|
||||
])
|
||||
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_authentication_in_multiple_threads(self, post):
|
||||
@mock.patch.object(sender, 'ClientV3', autospec=True)
|
||||
@mock.patch.object(plugin, 'LOGGER', autospec=True)
|
||||
@mock_collectd()
|
||||
@mock_config()
|
||||
@mock_value()
|
||||
def test_authentication_in_multiple_threads(
|
||||
self, data, config, collectd, LOGGER, ClientV3, post):
|
||||
"""Test authentication in muliple threads
|
||||
|
||||
This test simulates the authentication performed from different thread
|
||||
@ -289,17 +378,18 @@ class PluginTest(TestCase):
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
# init plugin instance
|
||||
self._init_instance()
|
||||
# init instance
|
||||
instance = plugin.Plugin(collectd=collectd, config=config)
|
||||
|
||||
# the sender used by the instance
|
||||
sender = self.plugin_instance._writer._sender
|
||||
sender = instance._writer._sender
|
||||
|
||||
# create a dummy lock
|
||||
class DummyLock(namedtuple('LockBase', ['sender', 'token', 'urlbase'])):
|
||||
"""Lock simulation, which sets the auth token when locked"""
|
||||
|
||||
def __enter__(self, *args, **kwargs):
|
||||
# pylint: disable=protected-access
|
||||
self.sender._auth_token = self.token
|
||||
self.sender._url_base = self.urlbase
|
||||
|
||||
@ -310,61 +400,110 @@ class PluginTest(TestCase):
|
||||
sender._auth_lock = DummyLock(sender, 'TOKEN', 'URLBASE/%s')
|
||||
|
||||
# write the value
|
||||
self._write_value(self._create_value())
|
||||
instance.write(data)
|
||||
|
||||
# verify the results
|
||||
client_class \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
|
||||
# No errors has been registered
|
||||
LOGGER.exception.assert_not_called()
|
||||
|
||||
# client has not been called at all
|
||||
self.assertFalse(client_class.called)
|
||||
ClientV3.assert_not_called()
|
||||
|
||||
# verify the auth token
|
||||
call_list = post.call_args_list
|
||||
self.assertEqual(len(call_list), 1)
|
||||
# 0 = first call > 1 = call kwargs > headers argument > auth token
|
||||
token = call_list[0][1]['headers']['X-Auth-Token']
|
||||
self.assertEqual(token, 'TOKEN')
|
||||
post.assert_called_once_with(
|
||||
'URLBASE/cpu.freq', data=mock.ANY,
|
||||
headers={
|
||||
'Content-type': 'application/json', 'X-Auth-Token': 'TOKEN'},
|
||||
timeout=1.0)
|
||||
|
||||
def test_exceptions(self):
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
@mock.patch.object(sender, 'ClientV3', autospec=True)
|
||||
@mock.patch.object(plugin, 'Writer', autospec=True)
|
||||
@mock.patch.object(plugin, 'LOGGER', autospec=True)
|
||||
@mock_collectd()
|
||||
@mock_config()
|
||||
@mock_value()
|
||||
def test_exceptions(
|
||||
self, data, config, collectd, LOGGER, Writer, ClientV3, post):
|
||||
"""Test exception raised during write and shutdown"""
|
||||
|
||||
self._init_instance()
|
||||
writer = Writer.return_value
|
||||
writer.write.side_effect = ValueError('Test write error')
|
||||
writer.flush.side_effect = RuntimeError('Test shutdown error')
|
||||
|
||||
writer = mock.Mock()
|
||||
writer.flush.side_effect = Exception('Test shutdown error')
|
||||
writer.write.side_effect = Exception('Test write error')
|
||||
# init instance
|
||||
instance = plugin.Plugin(collectd=collectd, config=config)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.plugin_instance._writer = writer
|
||||
# pylint: enable=protected-access
|
||||
self.assertRaises(ValueError, instance.write, data)
|
||||
self.assertRaises(RuntimeError, instance.shutdown)
|
||||
|
||||
self.plugin_instance.write(self._create_value())
|
||||
self.plugin_instance.shutdown()
|
||||
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
|
||||
@mock_collectd()
|
||||
def test_log_debug_to_collectd(self, collectd, ROOT_LOGGER):
|
||||
"""Verify that debug messages are sent to collectd."""
|
||||
|
||||
self.assertErrors([
|
||||
'Exception during write: Test write error',
|
||||
'Exception during shutdown: Test shutdown error'])
|
||||
plugin.register_plugin(collectd=collectd)
|
||||
|
||||
@staticmethod
|
||||
def _create_value():
|
||||
"""Create a value"""
|
||||
retval = Value()
|
||||
retval.plugin = 'cpu'
|
||||
retval.plugin_instance = '0'
|
||||
retval.type = 'freq'
|
||||
retval.add_value(1234)
|
||||
return retval
|
||||
# When log messages are produced
|
||||
ROOT_LOGGER.debug('some %s', 'noise')
|
||||
|
||||
def _init_instance(self):
|
||||
"""Init current plugin instance"""
|
||||
self.plugin_instance.config(self.config.node)
|
||||
self.plugin_instance.init()
|
||||
# When plugin function is called
|
||||
collectd.debug.assert_called_once_with('some noise')
|
||||
|
||||
def _write_value(self, value, errors=None):
|
||||
"""Write a value and verify result"""
|
||||
self.plugin_instance.write(value)
|
||||
if errors is None:
|
||||
self.assertNoError()
|
||||
else:
|
||||
self.assertErrors(errors)
|
||||
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
|
||||
@mock_collectd()
|
||||
def test_log_infos_to_collectd(self, collectd, ROOT_LOGGER):
|
||||
"""Verify that the callbacks are registered properly"""
|
||||
|
||||
plugin.register_plugin(collectd=collectd)
|
||||
|
||||
# When log messages are produced
|
||||
ROOT_LOGGER.info('%d info', 1)
|
||||
|
||||
# When plugin function is called
|
||||
collectd.info.assert_called_once_with('1 info')
|
||||
|
||||
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
|
||||
@mock_collectd()
|
||||
def test_log_errors_to_collectd(self, collectd, ROOT_LOGGER):
|
||||
"""Verify that the callbacks are registered properly"""
|
||||
|
||||
plugin.register_plugin(collectd=collectd)
|
||||
|
||||
# When log messages are produced
|
||||
ROOT_LOGGER.error('some error')
|
||||
|
||||
# When plugin function is called
|
||||
collectd.error.assert_called_once_with('some error')
|
||||
|
||||
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
|
||||
@mock_collectd()
|
||||
def test_log_fatal_to_collectd(self, collectd, ROOT_LOGGER):
|
||||
"""Verify that the callbacks are registered properly"""
|
||||
|
||||
plugin.register_plugin(collectd=collectd)
|
||||
|
||||
# When log messages are produced
|
||||
ROOT_LOGGER.fatal('some error')
|
||||
|
||||
# When plugin function is called
|
||||
collectd.error.assert_called_once_with('some error')
|
||||
|
||||
@mock.patch.object(plugin, 'ROOT_LOGGER', new_callable=Logger, name='me')
|
||||
@mock_collectd()
|
||||
def test_log_exceptions_to_collectd(self, collectd, ROOT_LOGGER):
|
||||
"""Verify that the callbacks are registered properly"""
|
||||
|
||||
plugin.register_plugin(collectd=collectd)
|
||||
|
||||
# When exception is logged
|
||||
try:
|
||||
raise ValueError('some error')
|
||||
except ValueError:
|
||||
ROOT_LOGGER.exception('got exception')
|
||||
|
||||
# When main function is called
|
||||
collectd.error.assert_called_once_with(
|
||||
match.wildcard('got exception\n'
|
||||
'Traceback (most recent call last):\n'
|
||||
'*'
|
||||
'ValueError: some error'))
|
||||
|
@ -16,7 +16,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collectd_ceilometer.sender import Sender
|
||||
from collectd_ceilometer.settings import Config
|
||||
from collections import defaultdict
|
||||
from collections import namedtuple
|
||||
import json
|
||||
@ -87,10 +86,11 @@ class SampleContainer(object):
|
||||
class Writer(object):
|
||||
"""Data collector"""
|
||||
|
||||
def __init__(self, meters):
|
||||
def __init__(self, meters, config):
|
||||
self._meters = meters
|
||||
self._samples = SampleContainer()
|
||||
self._sender = Sender()
|
||||
self._config = config
|
||||
|
||||
def write(self, vl, data):
|
||||
"""Collect data from collectd
|
||||
@ -123,8 +123,7 @@ class Writer(object):
|
||||
]
|
||||
|
||||
# add data to cache and get the samples to send
|
||||
to_send = self._samples.add(metername, data,
|
||||
Config.instance().BATCH_SIZE)
|
||||
to_send = self._samples.add(metername, data, self._config.BATCH_SIZE)
|
||||
if to_send:
|
||||
self._send_data(metername, to_send)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user