Emma Foley a00f360f8f Implement plugin
Add functionality to the plugin:
- Convert from collectd data sources to Ceilometer format
- Add unit mappings for Ceilometer
- Define resource IDs
- Add unit tests

Change-Id: Ica1f49ea3c9bbc4bc857044dea7da39869b33bba
2016-01-18 15:45:47 +00:00

365 lines
13 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"""
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
class PluginTest(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):
"""Verify that the callbacks are registered properly"""
collectd = self.get_mock('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)
def test_write(self):
"""Test collectd data writing"""
from collectd_ceilometer.sender import HTTP_CREATED
requests = self.get_mock('requests')
requests.post.return_value.status_code = HTTP_CREATED
requests.post.return_value.text = 'Created'
client_class \
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
auth_token = client_class.return_value.auth_token
# 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()
# no authentication has been performed so far
self.assertFalse(client_class.called)
# write first value
self._write_value(data)
# no value has been sent to ceilometer
self.assertFalse(requests.post.called)
# send the second value
self._write_value(data)
# authentication client has been created
self.assertTrue(client_class.called)
self.assertEqual(client_class.call_count, 1)
# and values has been sent
self.assertTrue(requests.post.called)
self.assertEqual(requests.post.call_count, 1)
expected_args = ('https://test-ceilometer.tld/v2/meters/cpu.freq',)
expected_kwargs = {
'data': [{
"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_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 = requests.post.call_args[1]
called_kwargs['data'] = json.loads(called_kwargs['data'])
# verify data sent to ceilometer
self.assertEqual(requests.post.call_args[0], expected_args)
self.assertEqual(called_kwargs, expected_kwargs)
# reset post method
requests.post.reset_mock()
# write another values
self._write_value(data)
# nothing has been sent
self.assertFalse(requests.post.called)
# call shutdown
self.plugin_instance.shutdown()
self.assertNoError()
# previously written value has been sent
self.assertTrue(requests.post.called)
# no more authentication required
self.assertEqual(client_class.call_count, 1)
expected_kwargs = {
'data': [{
"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 = requests.post.call_args[1]
called_kwargs['data'] = json.loads(called_kwargs['data'])
# verify data sent to ceilometer
self.assertEqual(requests.post.call_args[0], expected_args)
self.assertEqual(called_kwargs, expected_kwargs)
def test_write_auth_failed(self):
"""Test authentication failure"""
# 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')
# init instance
self._init_instance()
# write the value
errors = [
'Exception during write: Test Client() exception']
self._write_value(self._create_value(), errors)
# no requests method has been called
self.assertFalse(self.get_mock('requests').post.called,
"requests method has been called")
def test_write_auth_failed2(self):
"""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(
"Missing name 'xxx' in received services",
"exception",
"services list")
# init instance
self._init_instance()
# 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)
# no requests method has been called
self.assertFalse(self.get_mock('requests').post.called,
"requests method has been called")
def test_request_error(self):
"""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
requests = self.get_mock('requests')
requests.post.side_effect = RequestException('Test POST exception')
# init instance
self._init_instance()
# write the value
self._write_value(
self._create_value(),
['Ceilometer request error: Test POST exception'])
def test_reauthentication(self):
"""Test re-authentication"""
from collectd_ceilometer.sender import HTTP_UNAUTHORIZED
requests = self.get_mock('requests')
client_class \
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
client_class.return_value.auth_token = 'Test auth token'
# init instance
self._init_instance()
# write the first value
self._write_value(self._create_value())
# verify the auth token
call_list = requests.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')
# subsequent call of POST method will fail due to the authentication
requests.post.return_value.status_code = HTTP_UNAUTHORIZED
requests.post.return_value.text = 'Unauthorized'
# set a new auth token
client_class.return_value.auth_token = 'New test auth token'
self._write_value(self._create_value())
# verify the auth token
call_list = requests.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')
def test_authentication_in_multiple_threads(self):
"""Test authentication in muliple threads
This test simulates the authentication performed from different thread
after the authentication lock has been acquired. The sender is not
authenticated, the lock is acquired, the authentication token exists
(i.e. it has been set by different thread) and it is used.
"""
# pylint: disable=protected-access
# init plugin instance
self._init_instance()
# the sender used by the instance
sender = self.plugin_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):
self.sender._auth_token = self.token
self.sender._url_base = self.urlbase
def __exit__(self, *args, **kwargs):
pass
# replace the sender's lock by the dummy lock
sender._auth_lock = DummyLock(sender, 'TOKEN', 'URLBASE/%s')
# write the value
self._write_value(self._create_value())
# verify the results
requests = self.get_mock('requests')
client_class \
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
# client has not been called at all
self.assertFalse(client_class.called)
# verify the auth token
call_list = requests.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')
def test_exceptions(self):
"""Test exception raised during write and shutdown"""
self._init_instance()
writer = mock.Mock()
writer.flush.side_effect = Exception('Test shutdown error')
writer.write.side_effect = Exception('Test write error')
# pylint: disable=protected-access
self.plugin_instance._writer = writer
# pylint: enable=protected-access
self.plugin_instance.write(self._create_value())
self.plugin_instance.shutdown()
self.assertErrors([
'Exception during write: Test write error',
'Exception during shutdown: Test shutdown error'])
@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
def _init_instance(self):
"""Init current plugin instance"""
self.plugin_instance.config(self.config.node)
self.plugin_instance.init()
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)