diff --git a/collectd_ceilometer/aodh/sender.py b/collectd_ceilometer/aodh/sender.py index 4ae4329..d9a90f4 100644 --- a/collectd_ceilometer/aodh/sender.py +++ b/collectd_ceilometer/aodh/sender.py @@ -25,6 +25,7 @@ import requests import collectd_ceilometer from collectd_ceilometer.common import sender as common_sender from collectd_ceilometer.common.settings import Config +from collections import OrderedDict LOGGER = logging.getLogger(__name__) ROOT_LOGGER = logging.getLogger(collectd_ceilometer.__name__) @@ -114,24 +115,50 @@ class Sender(common_sender.Sender): Config.instance().CEILOMETER_URL_TYPE) return endpoint + def _get_remote_alarm_id(self, endpoint, alarm_name): + """Request alarm with given name.""" + url = "{}/v2/alarms".format(endpoint) + # request id from a server. openstack service can + # handle only predefined order of args: q.field=&q.op=&q.value= + # in other cases it will fail + params = OrderedDict([("q.field", "name"), ("q.op", "eq"), + ("q.value", alarm_name)]) + alarm_id = None + try: + result = self._perform_request(url, params, self._auth_token, + req_type="get") + except Exception as exc: + LOGGER.warn('Invalid response from server for alarm:' + ' %s error = %s %s' % (alarm_name, type(exc), exc)) + return None + + try: + # parse response + alarm_id = json.loads(result.text)[0]['alarm_id'] + except (KeyError, ValueError, IndexError): + LOGGER.warn('NO alarm on the server: %s' % alarm_name) + return alarm_id + def _get_alarm_id(self, alarm_name, severity, metername, alarm_severity): # check for an alarm and update try: return self._alarm_ids[alarm_name] - # or create a new alarm except KeyError as ke: LOGGER.warn(ke) - LOGGER.warn('No known ID for %s', alarm_name) + LOGGER.warn('No ID in a cache for %s', alarm_name) endpoint = self._get_endpoint("aodh") - alarm_id = \ - self._create_alarm(endpoint, severity, - metername, alarm_name, alarm_severity) + alarm_id = self._get_remote_alarm_id(endpoint, alarm_name) + # create new alarm + if alarm_id is None: + alarm_id = \ + self._create_alarm(endpoint, severity, + metername, alarm_name, alarm_severity) if alarm_id is not None: # Add alarm ids/names to relevant dictionaries/lists self._alarm_ids[alarm_name] = alarm_id - return None + return alarm_id def _create_alarm(self, endpoint, severity, metername, alarm_name, alarm_severity): diff --git a/collectd_ceilometer/common/sender.py b/collectd_ceilometer/common/sender.py index 346e3b0..f3112d9 100644 --- a/collectd_ceilometer/common/sender.py +++ b/collectd_ceilometer/common/sender.py @@ -177,13 +177,18 @@ class Sender(object): @classmethod def _perform_request(cls, url, payload, auth_token, req_type="post"): """Perform the POST/PUT request.""" - LOGGER.debug('Performing request to %s', url) + LOGGER.debug('Performing request to %s, payload=%s, req_type = %s' % + (url, payload, req_type)) # request headers headers = {'X-Auth-Token': auth_token, 'Content-type': 'application/json'} # perform request and return its result - if req_type == "put": + if req_type == "get": + response = requests.get( + url, params=payload, headers=headers, + timeout=(Config.instance().CEILOMETER_TIMEOUT / 1000.)) + elif req_type == "put": response = requests.put( url, data=payload, headers=headers, timeout=(Config.instance().CEILOMETER_TIMEOUT / 1000.)) diff --git a/collectd_ceilometer/tests/aodh/test_sender.py b/collectd_ceilometer/tests/aodh/test_sender.py new file mode 100644 index 0000000..ea9a15d --- /dev/null +++ b/collectd_ceilometer/tests/aodh/test_sender.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 2017 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. + +"""Sender tests.""" + +import mock +import requests +import unittest + +from collectd_ceilometer.aodh import sender as aodh_sender +from collections import OrderedDict + +valid_resp = '[{"alarm_actions": [], "event_rule": {"query": [],'\ + '"event_type": "events.gauge"}, "ok_actions": [],'\ + '"name": "alarm", "severity": "moderate",'\ + '"timestamp": "2017-08-22T06:22:46.790949", "enabled": true,'\ + '"alarm_id": "11af9327-8c3a-4120-8a74-bbc672c90f0a",'\ + '"time_constraints": [], "insufficient_data_actions": []}]' + +valid_alarm_id = "valid_alarm_id" + + +class response(object): + + __attrs__ = [ + '_content', 'status_code', 'headers', 'url', 'history', + 'encoding', 'reason', 'cookies', 'elapsed', 'request', 'text' + ] + + def __init__(self, text, code): + self.text = text + self.status_code = code + + def raise_for_status(self): + pass + + +class TestSender(unittest.TestCase): + """Test the Sender class.""" + + def setUp(self): + super(TestSender, self).setUp() + self.sender = aodh_sender.Sender() + + @mock.patch.object(aodh_sender.Sender, "_get_remote_alarm_id", + autospec=True) + @mock.patch.object(aodh_sender.Sender, "_get_endpoint", autospec=True) + @mock.patch.object(aodh_sender.Sender, "_create_alarm", spec=callable) + def test_get_alarm_id_no_local_alarm(self, _create_alarm, _get_endpoint, + _get_remote_alarm_id): + """Test the behaviour when the alarm id doesn't exist locally. + + Set-up: + Test: call _get_alarm_id when no local alarm exists. + Expected behaviour: + * _get_remote_alarm_id is called + * self._alarm_ids is updated + """ + _get_remote_alarm_id.return_value = valid_alarm_id + + alarm_id = self.sender._get_alarm_id("alarm", "critical", "link status", + "critical") + + self.assertEqual(alarm_id, valid_alarm_id, + "_get_remote_alarm_id is not called") + _get_remote_alarm_id.assert_called_once_with(mock.ANY, mock.ANY, + "alarm") + _create_alarm.assert_not_called() + _get_endpoint.assert_called_once_with(mock.ANY, "aodh") + self.assertIn("alarm", self.sender._alarm_ids, + "self._alarm_ids is not updated") + + @mock.patch.object(aodh_sender.Sender, "_create_alarm", spec=callable) + @mock.patch.object(requests, 'get', spec=callable) + def test_get_remote_alarm_id(self, get, _create_alarm): + """Test behaviour of _get_remote_alarm_id + + Set-up: + Test: Call _get_remote_alarm_id with typical parameters + Expected behaviour: + * requests.get is called with correct args + * Alarm ID is returned + """ + resp = response(valid_resp, 200) + get.return_value = resp + params = OrderedDict([(u"q.field", u"name"), (u"q.op", u"eq"), + (u"q.value", u"alarm")]) + + alarm_id = self.sender._get_remote_alarm_id(u"endpoint", u"alarm") + + self.assertEqual(alarm_id, "11af9327-8c3a-4120-8a74-bbc672c90f0a", + "invalid alarm id") + _create_alarm.assert_not_called() + get.assert_called_once_with(u"endpoint/v2/alarms", params=params, + headers=mock.ANY, timeout=mock.ANY) + + @mock.patch.object(aodh_sender.Sender, "_get_endpoint", autospec=True) + @mock.patch.object(aodh_sender.Sender, "_create_alarm", spec=callable) + @mock.patch.object(requests, 'get', spec=callable) + def test_get_alarm_id_not_found(self, get, _create_alarm, _get_endpoint): + """Test behaviour of _get_alarm_id when alarm does not exist + + Set up: + Test: + * call _get_alarm_id + * requests.get/sender._perform_request return an error + Expected behaviour: _create_alarm is called + """ + resp = response("some invalid response", 404) + get.return_value = resp + _create_alarm.return_value = valid_alarm_id + + alarm_id = self.sender._get_alarm_id("alarm", "critical", "link status", + "critical") + + _create_alarm.assert_called_once_with( + mock.ANY, "critical", "link status", "alarm", "critical") + _get_endpoint.assert_called_once_with(mock.ANY, "aodh") + self.assertEqual(alarm_id, valid_alarm_id, "invalid alarm id") diff --git a/collectd_ceilometer/tests/common/test_sender.py b/collectd_ceilometer/tests/common/test_sender.py new file mode 100644 index 0000000..cba4d21 --- /dev/null +++ b/collectd_ceilometer/tests/common/test_sender.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 2017 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. + +"""Sender tests.""" + +import mock +import requests +import unittest + +from collectd_ceilometer.common import sender as common_sender + + +class TestSender(unittest.TestCase): + """Test the Sender class.""" + + def setUp(self): + super(TestSender, self).setUp() + self.sender = common_sender.Sender() + + @mock.patch.object(requests, 'post') + @mock.patch.object(requests, 'get') + def test_perform_request_req_type_get(self, get, post): + """Test the behaviour when performing a get request + + Set-up: None + Test: call _perform_request with req_type="get" + Expected behaviour: + * requests.get is called with appropriate params + * requests.post is not called + """ + self.sender._perform_request("my-url", "some payload", + "some headers", req_type="get") + + post.assert_not_called() + get.assert_called_with("my-url", params="some payload", + headers=mock.ANY, timeout=mock.ANY) + + @mock.patch.object(requests, 'post') + @mock.patch.object(requests, 'get') + def test_perform_request_req_type_post(self, get, post): + """Test the behaviour when performing a post request + + Set-up: None + Test: call _perform_request with req_type="post" + Expected behaviour: + * requests.get is not called + * requests.post is called with appropriate params + """ + self.sender._perform_request("my-url", "some payload", + "some headers", req_type="post") + + get.assert_not_called() + post.assert_called_with("my-url", data="some payload", + headers=mock.ANY, timeout=mock.ANY)