Refactor update and create alarm functions

- Checks for errors and retries creating/updating alarms which fixes
    the bug, by removing the need to have a dictionary for alarms.
  - Included a get_alarm_id function.
  - Refactored the update and create alarm functions.
  - Updated and included the relevant tests for this.
  - Include a reno for the bug fix.

Change-Id: I7e998bc8cb55f9ef7564464fcd4eded06643e1eb
Closes-Bug: #1677597
This commit is contained in:
Helena McGough 2017-05-17 10:44:43 +00:00 committed by Helena
parent e64fbd519f
commit 3b88d50820
3 changed files with 108 additions and 115 deletions

View File

@ -52,7 +52,6 @@ class Sender(object):
self._auth_lock = threading.Lock() self._auth_lock = threading.Lock()
self._failed_auth = False self._failed_auth = False
self._alarm_ids = {} self._alarm_ids = {}
self._alarm_names = list()
def _authenticate(self): def _authenticate(self):
"""Authenticate and renew the authentication token.""" """Authenticate and renew the authentication token."""
@ -131,13 +130,9 @@ class Sender(object):
# Create alarm name # Create alarm name
alarm_name = self._get_alarm_name(metername, resource_id) alarm_name = self._get_alarm_name(metername, resource_id)
# check for and/or get alarm_id # Update or create this alarm
alarm_id = self._get_alarm_id(alarm_name, severity, metername, message) result = self._update_or_create_alarm(alarm_name, message, auth_token,
severity, metername)
if alarm_id is not None:
result = self._update_alarm(alarm_id, message, auth_token)
else:
result = None
if result is None: if result is None:
return return
@ -159,25 +154,19 @@ class Sender(object):
auth_token = self._authenticate() auth_token = self._authenticate()
if auth_token is not None: if auth_token is not None:
if alarm_id is not None: result = self._update_or_create_alarm(alarm_name, message,
result = self._update_alarm(alarm_id, message, auth_token) auth_token, severity,
else: metername)
result = None
if result.status_code == HTTP_NOT_FOUND: if result.status_code == HTTP_NOT_FOUND:
LOGGER.debug("Received 404 error when submitting %s sample, \ LOGGER.debug("Received 404 error when submitting %s notification, \
creating a new metric", creating a new alarm",
alarm_name) alarm_name)
# check for and/or get alarm_id # check for and/or get alarm_id
alarm_id = self._get_alarm_id(alarm_name, severity, result = self._update_or_create_alarm(alarm_name, message,
metername, message) auth_token, severity,
metername)
LOGGER.info('alarmname: %s, alarm_id: %s', alarm_name, alarm_id)
if alarm_id is not None:
result = self._update_alarm(alarm_id, message, auth_token)
else:
result = None
if result.status_code == HTTP_CREATED: if result.status_code == HTTP_CREATED:
LOGGER.debug('Result: %s', HTTP_CREATED) LOGGER.debug('Result: %s', HTTP_CREATED)
@ -193,22 +182,23 @@ class Sender(object):
Config.instance().CEILOMETER_URL_TYPE) Config.instance().CEILOMETER_URL_TYPE)
return endpoint return endpoint
def _get_alarm_id(self, alarm_name, severity, metername, message): def _update_or_create_alarm(self, alarm_name, message, auth_token,
"""Check for existing alarm and its id or create a new one.""" severity, metername):
# check for an alarm and update
try: try:
return self._alarm_ids[alarm_name] alarm_id = self._get_alarm_id(alarm_name)
result = self._update_alarm(alarm_id, message, auth_token)
# or create a new alarm
except KeyError as ke: except KeyError as ke:
LOGGER.warn(ke) LOGGER.warn(ke)
endpoint = self._get_endpoint("aodh") endpoint = self._get_endpoint("aodh")
if alarm_name not in self._alarm_names: LOGGER.warn('No known ID for %s', alarm_name)
LOGGER.warn('No known ID for %s', alarm_name) result, self._alarm_ids[alarm_name] = \
self._alarm_names.append(alarm_name) self._create_alarm(endpoint, severity,
self._alarm_ids[alarm_name] = \ metername, alarm_name, message)
self._create_alarm(endpoint, severity, return result
metername, alarm_name, message)
return None
def _create_alarm(self, endpoint, severity, metername, def _create_alarm(self, endpoint, severity, metername,
alarm_name, message): alarm_name, message):
@ -226,7 +216,11 @@ class Sender(object):
result = self._perform_post_request(url, payload, self._auth_token) result = self._perform_post_request(url, payload, self._auth_token)
alarm_id = json.loads(result.text)['alarm_id'] alarm_id = json.loads(result.text)['alarm_id']
LOGGER.debug("alarm_id=%s", alarm_id) LOGGER.debug("alarm_id=%s", alarm_id)
return alarm_id return result, alarm_id
def _get_alarm_id(self, alarm_name):
"""Try and return an alarm_id for an collectd notification"""
return self._alarm_ids[alarm_name]
def _get_alarm_state(self, message): def _get_alarm_state(self, message):
"""Get the state of the alarm.""" """Get the state of the alarm."""
@ -275,11 +269,10 @@ class Sender(object):
except RequestException as exc: except RequestException as exc:
LOGGER.error('aodh request error: %s', six.text_type(exc)) LOGGER.error('aodh request error: %s', six.text_type(exc))
finally: finally:
if response is not None: LOGGER.debug(
LOGGER.debug( "Returning response from _perform_post_request(): %s",
"Returning response from _perform_post_request(): %s", response.status_code)
response.status_code) return response
return response
@classmethod @classmethod
def _perform_update_request(cls, url, auth_token, payload): def _perform_update_request(cls, url, auth_token, payload):
@ -304,8 +297,7 @@ class Sender(object):
except RequestException as exc: except RequestException as exc:
LOGGER.error('aodh request error: %s', six.text_type(exc)) LOGGER.error('aodh request error: %s', six.text_type(exc))
finally: finally:
if response is not None: LOGGER.debug(
LOGGER.debug( 'Returning response from _perform_update_request(): %s',
'Returning response from _perform_update_request(): %s', response.status_code)
response.status_code) return response
return response

View File

@ -25,6 +25,7 @@ import unittest
from collectd_ceilometer.aodh import plugin from collectd_ceilometer.aodh import plugin
from collectd_ceilometer.aodh import sender from collectd_ceilometer.aodh import sender
from collectd_ceilometer.common.keystone_light import KeystoneException from collectd_ceilometer.common.keystone_light import KeystoneException
from collectd_ceilometer.common.meters import base
Logger = logging.getLoggerClass() Logger = logging.getLoggerClass()
@ -136,58 +137,55 @@ class TestPlugin(unittest.TestCase):
collectd.register_notification.assert_called_once_with(instance.notify) collectd.register_notification.assert_called_once_with(instance.notify)
collectd.register_shutdown.assert_called_once_with(instance.shutdown) collectd.register_shutdown.assert_called_once_with(instance.shutdown)
@mock.patch.object(sender.Sender, '_get_alarm_id', autospec=True) @mock.patch.object(sender.Sender, '_update_or_create_alarm', autospec=True)
@mock.patch.object(requests, 'put', spec=callable) @mock.patch.object(sender.Sender, '_get_alarm_name', autospec=True)
@mock.patch.object(base, 'Meter', autospec=True)
@mock.patch.object(sender, 'ClientV3', autospec=True) @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock_collectd() @mock_collectd()
@mock_config() @mock_config()
@mock_value() @mock_value()
def test_notify(self, data, config, collectd, ClientV3, put, def test_update_or_create_alarm(self, data, config, collectd,
_get_alarm_id): ClientV3, meter,
"""Test collectd data notifying.""" _get_alarm_name, _update_or_create_alarm):
"""Test the update/create alarm function"""
auth_client = ClientV3.return_value auth_client = ClientV3.return_value
auth_client.get_service_endpoint.return_value = \ auth_client.get_service_endpoint.return_value = \
'https://test-aodh.tld' 'https://test-aodh.tld'
put.return_value.status_code = sender.HTTP_CREATED _update_or_create_alarm.return_value = requests.Response()
put.return_value.text = 'Updated'
# Test when the value doesn't get updated # init sender instance
_get_alarm_id.return_value = None instance = sender.Sender()
# init instance _get_alarm_name.return_value = 'my-alarm'
instance = plugin.Plugin(collectd=collectd, config=config) meter_name = meter.meter_name.return_value
message = meter.message.return_value
severity = meter.severity.return_value
resource_id = meter.resource_id.return_value
# send the value(doesn't update) # send the values
instance.notify(data) instance.send(meter_name, severity, resource_id, message)
collectd.error.assert_not_called()
put.assert_not_called() # check that the function is called
_update_or_create_alarm.assert_called_once_with(
instance, 'my-alarm', message, auth_client.auth_token,
severity, meter_name)
# Test when an alarm will be updated # reset function
_get_alarm_id.return_value = 'my-alarm-id' _update_or_create_alarm.reset_mock()
# send the value(updates) # run test again for failed attempt
instance.notify(data) _update_or_create_alarm.return_value = None
collectd.error.assert_not_called()
# authentication client that has been created instance.send(meter_name, severity, resource_id, message)
ClientV3.assert_called_once()
# and values that have been sent # and values that have been sent
put.assert_called_once_with( _update_or_create_alarm.assert_called_once_with(
'https://test-aodh.tld' + instance, 'my-alarm', message, auth_client.auth_token,
'/v2/alarms/my-alarm-id/state', severity, meter_name)
data='"insufficient data"',
headers={'Content-type': 'application/json',
'X-Auth-Token': auth_client.auth_token},
timeout=1.0)
# reset post method # reset post method
put.reset_mock() _update_or_create_alarm.reset_mock()
# call shutdown
instance.shutdown()
@mock.patch.object(requests, 'put', spec=callable) @mock.patch.object(requests, 'put', spec=callable)
@mock.patch.object(sender, 'ClientV3', autospec=True) @mock.patch.object(sender, 'ClientV3', autospec=True)
@ -237,20 +235,17 @@ class TestPlugin(unittest.TestCase):
# the value # the value
self.assertRaises(requests.RequestException, instance.notify, data) self.assertRaises(requests.RequestException, instance.notify, data)
@mock.patch.object(sender.Sender, '_get_alarm_id', autospec=True) @mock.patch.object(sender.Sender, '_update_or_create_alarm', autospec=True)
@mock.patch.object(requests, 'put', spec=callable) @mock.patch.object(sender.Sender, '_get_alarm_name', autospec=True)
@mock.patch.object(base, 'Meter', autospec=True)
@mock.patch.object(sender, 'ClientV3', autospec=True) @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock_collectd() @mock_collectd()
@mock_config() @mock_config()
@mock_value() @mock_value()
def test_reauthentication(self, data, config, collectd, def test_reauthentication(self, data, config, collectd,
ClientV3, put, _get_alarm_id): ClientV3, meter, _get_alarm_name,
_update_or_create_alarm):
"""Test re-authentication for update request.""" """Test re-authentication for update request."""
# init instance
instance = plugin.Plugin(collectd=collectd, config=config)
# test for non update request
_get_alarm_id.return_value = None
# response returned on success # response returned on success
response_ok = requests.Response() response_ok = requests.Response()
@ -260,19 +255,29 @@ class TestPlugin(unittest.TestCase):
response_unauthorized = requests.Response() response_unauthorized = requests.Response()
response_unauthorized.status_code = requests.codes["UNAUTHORIZED"] response_unauthorized.status_code = requests.codes["UNAUTHORIZED"]
put.return_value = response_ok _update_or_create_alarm.return_value = response_ok
client = ClientV3.return_value client = ClientV3.return_value
client.auth_token = 'Test auth token' client.auth_token = 'Test auth token'
# notify of the value # init instance attempt to update/create alarm
instance.notify(data) instance = sender.Sender()
# de-assert the non-update request alarm_name = _get_alarm_name.return_value
put.assert_not_called() meter_name = meter.meter_name.return_value
message = meter.message.return_value
severity = meter.severity.return_value
resource_id = meter.resource_id.return_value
# test for update request # send the data
_get_alarm_id.return_value = 'my-alarm-id' instance.send(meter_name, severity, resource_id, message)
_update_or_create_alarm.assert_called_once_with(
instance, alarm_name, message, client.auth_token,
severity, meter_name)
# de-assert the request
_update_or_create_alarm.reset_mock()
# response returned on success # response returned on success
response_ok = requests.Response() response_ok = requests.Response()
@ -282,40 +287,28 @@ class TestPlugin(unittest.TestCase):
response_unauthorized = requests.Response() response_unauthorized = requests.Response()
response_unauthorized.status_code = requests.codes["UNAUTHORIZED"] response_unauthorized.status_code = requests.codes["UNAUTHORIZED"]
put.return_value = response_ok _update_or_create_alarm.return_value = response_ok
client = ClientV3.return_value client = ClientV3.return_value
client.auth_token = 'Test auth token' client.auth_token = 'Test auth token'
# notify of the value # send the data
instance.notify(data) instance.send(meter_name, severity, resource_id, message)
# verify the auth token _update_or_create_alarm.assert_called_once_with(
put.assert_called_once_with( instance, alarm_name, message, client.auth_token,
mock.ANY, data=mock.ANY, severity, meter_name)
headers={u'Content-type': mock.ANY,
u'X-Auth-Token': 'Test auth token'},
timeout=1.0)
# POST response is unauthorized -> new token needs to be acquired # update/create response is unauthorized -> new token needs
put.side_effect = [response_unauthorized, response_ok] # to be acquired
_update_or_create_alarm.side_effect = [response_unauthorized,
response_ok]
# set a new auth token # set a new auth token
client.auth_token = 'New test auth token' client.auth_token = 'New test auth token'
instance.notify(data) # send the data again
instance.send(meter_name, severity, resource_id, message)
# verify the auth token:
call_list = put.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')
@mock.patch.object(sender, 'ClientV3', autospec=True) @mock.patch.object(sender, 'ClientV3', autospec=True)
@mock.patch.object(plugin, 'Notifier', autospec=True) @mock.patch.object(plugin, 'Notifier', autospec=True)

View File

@ -0,0 +1,8 @@
---
fixes:
- |
Fixes 'bug 1677597 https://bugs.launchpad.net/collectd-ceilometer-plugin/+bug/1677597'
Refactored the update_alarm and create_alarm functions to allow for error
checking by introducing a update_or_create_alarm function. This allows
retrying updating or creating alarms if they fail. Also updated the
corresponding tests.