Fix POST result verification.
The result of POST request is threated using a more explicit approach via raise_for_status method instead of using ambigous bool operator. Requests library is mocked in plugin test instead of base test class. The test for this behaviour has been fixed. Closes-Bug: #1614772 Change-Id: I9079246b1b501b69e850878bef13c16c7c6d0b59
This commit is contained in:
@@ -16,21 +16,32 @@
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collectd_ceilometer.keystone_light import ClientV2 as keystoneClientV2
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import requests
|
||||
import six
|
||||
|
||||
from collectd_ceilometer.keystone_light import ClientV2
|
||||
from collectd_ceilometer.keystone_light import KeystoneException
|
||||
from collectd_ceilometer.settings import Config
|
||||
import logging
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
import six
|
||||
import threading
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# HTTP status codes
|
||||
HTTP_CREATED = 201
|
||||
HTTP_UNAUTHORIZED = 401
|
||||
HTTP_CREATED = requests.codes['CREATED']
|
||||
HTTP_UNAUTHORIZED = requests.codes['UNAUTHORIZED']
|
||||
|
||||
# Lookup dictionary for getting code status name from code status number
|
||||
# this is the inverse mapping of requests.codes dictionary
|
||||
STATUS_NAMES = {
|
||||
status_code: status_name
|
||||
for status_name, status_code in six.iteritems(requests.codes.__dict__)}
|
||||
|
||||
|
||||
def get_status_name(status_code):
|
||||
"""Get an human friendly name for given status code"""
|
||||
return STATUS_NAMES.get(status_code, str(status_code))
|
||||
|
||||
|
||||
class Sender(object):
|
||||
@@ -66,7 +77,7 @@ class Sender(object):
|
||||
# create a keystone client if it doesn't exist
|
||||
if self._keystone is None:
|
||||
cfg = Config.instance()
|
||||
self._keystone = keystoneClientV2(
|
||||
self._keystone = ClientV2(
|
||||
auth_url=cfg.OS_AUTH_URL,
|
||||
username=cfg.OS_USERNAME,
|
||||
password=cfg.OS_PASSWORD,
|
||||
@@ -125,39 +136,34 @@ class Sender(object):
|
||||
url = self._url_base % metername
|
||||
|
||||
# send the POST request
|
||||
result = self._perform_request(url, payload, auth_token)
|
||||
if not result:
|
||||
return
|
||||
try:
|
||||
return self._perform_request(url, payload, auth_token)
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
response = exc.response
|
||||
|
||||
# if the request failed due to an auth error
|
||||
if result.status_code == HTTP_UNAUTHORIZED:
|
||||
# reset the auth token in order to force the subsequent
|
||||
# _authenticate() call to renew it
|
||||
# Here, it can happen that the token is reset right after
|
||||
# another thread has finished the authentication and thus
|
||||
# the authentication may be performed twice
|
||||
self._auth_token = None
|
||||
# if the request failed due to an auth error
|
||||
if response.status_code == HTTP_UNAUTHORIZED:
|
||||
LOGGER.info('Renewing authentication.')
|
||||
|
||||
LOGGER.debug('Result: %s %s',
|
||||
six.text_type(result.status_code),
|
||||
result.text)
|
||||
# reset the auth token in order to force the subsequent
|
||||
# _authenticate() call to renew it
|
||||
# Here, it can happen that the token is reset right after
|
||||
# another thread has finished the authentication and thus
|
||||
# the authentication may be performed twice
|
||||
self._auth_token = None
|
||||
|
||||
# renew the authentication token
|
||||
auth_token = self._authenticate()
|
||||
# renew the authentication token
|
||||
auth_token = self._authenticate()
|
||||
|
||||
if auth_token is not None:
|
||||
# and try to repost
|
||||
result = self._perform_request(url, payload, auth_token)
|
||||
if auth_token is not None:
|
||||
# and try to repost
|
||||
return self._perform_request(url, payload, auth_token)
|
||||
else:
|
||||
# This is an error and it has to be forwarded
|
||||
raise
|
||||
|
||||
if result.status_code == HTTP_CREATED:
|
||||
LOGGER.debug('Result: %s', HTTP_CREATED)
|
||||
else:
|
||||
LOGGER.info('Result: %s %s',
|
||||
result.status_code,
|
||||
result.text)
|
||||
|
||||
@classmethod
|
||||
def _perform_request(cls, url, payload, auth_token):
|
||||
@staticmethod
|
||||
def _perform_request(url, payload, auth_token):
|
||||
"""Perform the POST request"""
|
||||
|
||||
LOGGER.debug('Performing request to %s', url)
|
||||
@@ -166,10 +172,23 @@ class Sender(object):
|
||||
headers = {'X-Auth-Token': auth_token,
|
||||
'Content-type': 'application/json'}
|
||||
# perform request and return its result
|
||||
response = requests.post(
|
||||
url, data=payload, headers=headers,
|
||||
timeout=(Config.instance().CEILOMETER_TIMEOUT / 1000.))
|
||||
|
||||
# Raises exception if there was an error
|
||||
try:
|
||||
return requests.post(
|
||||
url, data=payload, headers=headers,
|
||||
timeout=(Config.instance().CEILOMETER_TIMEOUT / 1000.))
|
||||
except RequestException as exc:
|
||||
LOGGER.error('Ceilometer request error: %s', six.text_type(exc))
|
||||
return None
|
||||
response.raise_for_status()
|
||||
# pylint: disable=broad-except
|
||||
except Exception:
|
||||
exc_info = 1
|
||||
raise
|
||||
else:
|
||||
exc_info = 0
|
||||
finally:
|
||||
# Log out the result of the request for debugging purpose
|
||||
LOGGER.debug(
|
||||
'Result: %s, %d, %r',
|
||||
get_status_name(response.status_code),
|
||||
response.status_code, response.text, exc_info=exc_info)
|
||||
return response
|
||||
|
||||
@@ -130,16 +130,10 @@ class TestCase(unittest.TestCase):
|
||||
|
||||
super(TestCase, self).setUp()
|
||||
|
||||
modules = ['collectd', 'libvirt', 'requests',
|
||||
'collectd_ceilometer.keystone_light']
|
||||
modules = ['collectd', 'libvirt', 'collectd_ceilometer.keystone_light']
|
||||
|
||||
self._mocked = {module: Mock() for module in modules}
|
||||
|
||||
# requests
|
||||
requests = self.get_mock('requests')
|
||||
requests.exceptions.RequestException = Exception
|
||||
self._mocked.update({'requests.exceptions': requests.exceptions})
|
||||
|
||||
keystone = self.get_mock('collectd_ceilometer.keystone_light')
|
||||
keystone.KeystoneException = KeystoneException
|
||||
self._mocked.update(
|
||||
|
||||
@@ -24,6 +24,7 @@ from collectd_ceilometer.tests.base import Value
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import mock
|
||||
import requests
|
||||
|
||||
|
||||
class PluginTest(TestCase):
|
||||
@@ -53,13 +54,13 @@ class PluginTest(TestCase):
|
||||
self.assertTrue(collectd.register_write.called)
|
||||
self.assertTrue(collectd.register_shutdown.called)
|
||||
|
||||
def test_write(self):
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_write(self, post):
|
||||
"""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'
|
||||
post.return_value = response = requests.Response()
|
||||
response.status_code = HTTP_CREATED
|
||||
|
||||
client_class \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
|
||||
@@ -79,7 +80,7 @@ class PluginTest(TestCase):
|
||||
self._write_value(data)
|
||||
|
||||
# no value has been sent to ceilometer
|
||||
self.assertFalse(requests.post.called)
|
||||
post.assert_not_called()
|
||||
|
||||
# send the second value
|
||||
self._write_value(data)
|
||||
@@ -88,8 +89,7 @@ class PluginTest(TestCase):
|
||||
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)
|
||||
post.assert_called_once()
|
||||
|
||||
expected_args = ('https://test-ceilometer.tld/v2/meters/cpu.freq',)
|
||||
expected_kwargs = {
|
||||
@@ -118,26 +118,26 @@ class PluginTest(TestCase):
|
||||
|
||||
# we cannot compare JSON directly because the original data
|
||||
# dictionary is unordered
|
||||
called_kwargs = requests.post.call_args[1]
|
||||
called_kwargs = 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(post.call_args[0], expected_args)
|
||||
self.assertEqual(called_kwargs, expected_kwargs)
|
||||
|
||||
# reset post method
|
||||
requests.post.reset_mock()
|
||||
post.reset_mock()
|
||||
|
||||
# write another values
|
||||
self._write_value(data)
|
||||
# nothing has been sent
|
||||
self.assertFalse(requests.post.called)
|
||||
post.assert_not_called()
|
||||
|
||||
# call shutdown
|
||||
self.plugin_instance.shutdown()
|
||||
self.assertNoError()
|
||||
# previously written value has been sent
|
||||
self.assertTrue(requests.post.called)
|
||||
post.assert_called_once()
|
||||
# no more authentication required
|
||||
self.assertEqual(client_class.call_count, 1)
|
||||
|
||||
@@ -158,14 +158,15 @@ class PluginTest(TestCase):
|
||||
|
||||
# we cannot compare JSON directly because the original data
|
||||
# dictionary is unordered
|
||||
called_kwargs = requests.post.call_args[1]
|
||||
called_kwargs = 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(post.call_args[0], expected_args)
|
||||
self.assertEqual(called_kwargs, expected_kwargs)
|
||||
|
||||
def test_write_auth_failed(self):
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_write_auth_failed(self, post):
|
||||
"""Test authentication failure"""
|
||||
|
||||
# tell the auth client to rise an exception
|
||||
@@ -182,10 +183,10 @@ class PluginTest(TestCase):
|
||||
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")
|
||||
post.assert_not_called()
|
||||
|
||||
def test_write_auth_failed2(self):
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_write_auth_failed2(self, post):
|
||||
"""Test authentication failure2"""
|
||||
|
||||
# tell the auth client to rise an exception
|
||||
@@ -209,18 +210,17 @@ class PluginTest(TestCase):
|
||||
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")
|
||||
post.assert_not_called()
|
||||
|
||||
def test_request_error(self):
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_request_error(self, 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
|
||||
requests = self.get_mock('requests')
|
||||
requests.post.side_effect = RequestException('Test POST exception')
|
||||
post.side_effect = RequestException('Test POST exception')
|
||||
|
||||
# init instance
|
||||
self._init_instance()
|
||||
@@ -228,13 +228,12 @@ class PluginTest(TestCase):
|
||||
# write the value
|
||||
self._write_value(
|
||||
self._create_value(),
|
||||
['Ceilometer request error: Test POST exception'])
|
||||
['Exception during write: Test POST exception'])
|
||||
|
||||
def test_reauthentication(self):
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_reauthentication(self, post):
|
||||
"""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'
|
||||
@@ -242,26 +241,33 @@ class PluginTest(TestCase):
|
||||
# init instance
|
||||
self._init_instance()
|
||||
|
||||
# write the first value
|
||||
# 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"]
|
||||
|
||||
# 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())
|
||||
|
||||
# verify the auth token
|
||||
call_list = requests.post.call_args_list
|
||||
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')
|
||||
|
||||
# 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
|
||||
call_list = post.call_args_list
|
||||
|
||||
# POST called three times
|
||||
self.assertEqual(len(call_list), 3)
|
||||
@@ -272,7 +278,8 @@ class PluginTest(TestCase):
|
||||
token = call_list[2][1]['headers']['X-Auth-Token']
|
||||
self.assertEqual(token, 'New test auth token')
|
||||
|
||||
def test_authentication_in_multiple_threads(self):
|
||||
@mock.patch.object(requests, 'post', spec=callable)
|
||||
def test_authentication_in_multiple_threads(self, post):
|
||||
"""Test authentication in muliple threads
|
||||
|
||||
This test simulates the authentication performed from different thread
|
||||
@@ -306,7 +313,6 @@ class PluginTest(TestCase):
|
||||
self._write_value(self._create_value())
|
||||
|
||||
# verify the results
|
||||
requests = self.get_mock('requests')
|
||||
client_class \
|
||||
= self.get_mock('collectd_ceilometer.keystone_light').ClientV2
|
||||
|
||||
@@ -314,7 +320,7 @@ class PluginTest(TestCase):
|
||||
self.assertFalse(client_class.called)
|
||||
|
||||
# verify the auth token
|
||||
call_list = requests.post.call_args_list
|
||||
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']
|
||||
|
||||
Reference in New Issue
Block a user