diff --git a/ceilometer/dispatcher/http.py b/ceilometer/dispatcher/http.py new file mode 100755 index 000000000..ec858e157 --- /dev/null +++ b/ceilometer/dispatcher/http.py @@ -0,0 +1,119 @@ +# Copyright 2013 IBM Corp +# +# Author: Tong Li +# +# 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 json + +from oslo.config import cfg +import requests + +from ceilometer import dispatcher +from ceilometer.openstack.common.gettextutils import _ +from ceilometer.openstack.common import log +from ceilometer.publisher import utils as publisher_utils + +LOG = log.getLogger(__name__) + +http_dispatcher_opts = [ + cfg.StrOpt('target', + default='', + help='The target where the http request will be sent to. ' + 'If this is not set, no data will be posted. For ' + 'example: target = http://hostname:1234/path'), + cfg.BoolOpt('cadf_only', + default=False, + help='The flag which indicates if only cadf message should ' + 'be posted. If false, all meters will be posted.'), + cfg.IntOpt('timeout', + default=5, + help='The max time in second to wait for a request to ' + 'timeout.'), +] + +cfg.CONF.register_opts(http_dispatcher_opts, group="dispatcher_http") + + +class HttpDispatcher(dispatcher.Base): + """Dispatcher class for posting metering data into a http target. + + To enable this dispatcher, the following option needs to be present in + ceilometer.conf file + + dispatchers = http + + Dispatcher specific options can be added as follows: + [dispatcher_http] + target = www.example.com + cadf_only = true + timeout = 2 + """ + def __init__(self, conf): + super(HttpDispatcher, self).__init__(conf) + self.headers = {'Content-type': 'application/json'} + self.timeout = self.conf.dispatcher_http.timeout + self.target = self.conf.dispatcher_http.target + self.cadf_only = self.conf.dispatcher_http.cadf_only + + def record_metering_data(self, data): + if self.target == '': + # if the target was not set, do not do anything + LOG.error(_('Dispatcher target was not set, no meter will ' + 'be posted. Set the target in the ceilometer.conf ' + 'file')) + return + + # We may have receive only one counter on the wire + if not isinstance(data, list): + data = [data] + + for meter in data: + LOG.debug(_( + 'metering data %(counter_name)s ' + 'for %(resource_id)s @ %(timestamp)s: %(counter_volume)s') + % ({'counter_name': meter['counter_name'], + 'resource_id': meter['resource_id'], + 'timestamp': meter.get('timestamp', 'NO TIMESTAMP'), + 'counter_volume': meter['counter_volume']})) + if publisher_utils.verify_signature( + meter, + self.conf.publisher.metering_secret): + try: + if self.cadf_only: + # Only cadf messages are being wanted. + req_data = meter.get('resource_metadata', + {}).get('request') + if req_data and 'CADF_EVENT' in req_data: + data = req_data['CADF_EVENT'] + else: + continue + else: + # Every meter should be posted to the target + data = meter + res = requests.post(self.target, + data=json.dumps(data), + headers=self.headers, + timeout=self.timeout) + LOG.debug(_('Message posting finished with status code ' + '%d.') % res.status_code) + except Exception as err: + LOG.exception(_('Failed to record metering data: %s'), + err) + else: + LOG.warning(_( + 'message signature invalid, discarding message: %r'), + meter) + + def record_events(self, events): + pass diff --git a/ceilometer/tests/dispatcher/test_http.py b/ceilometer/tests/dispatcher/test_http.py new file mode 100755 index 000000000..5f69af840 --- /dev/null +++ b/ceilometer/tests/dispatcher/test_http.py @@ -0,0 +1,126 @@ +# +# Copyright 2013 IBM Corp +# +# Author: Tong Li +# +# 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 mock +from oslo.config import fixture as fixture_config +from oslotest import base +import requests + +from ceilometer.dispatcher import http +from ceilometer.publisher import utils + + +class TestDispatcherHttp(base.BaseTestCase): + + def setUp(self): + super(TestDispatcherHttp, self).setUp() + self.CONF = self.useFixture(fixture_config.Config()).conf + self.msg = {'counter_name': 'test', + 'resource_id': self.id(), + 'counter_volume': 1, + } + self.msg['message_signature'] = utils.compute_signature( + self.msg, + self.CONF.publisher.metering_secret, + ) + + def test_http_dispatcher_config_options(self): + self.CONF.dispatcher_http.target = 'fake' + self.CONF.dispatcher_http.timeout = 2 + self.CONF.dispatcher_http.cadf_only = True + dispatcher = http.HttpDispatcher(self.CONF) + + self.assertEqual('fake', dispatcher.target) + self.assertEqual(2, dispatcher.timeout) + self.assertEqual(True, dispatcher.cadf_only) + + def test_http_dispatcher_with_no_target(self): + self.CONF.dispatcher_http.target = '' + dispatcher = http.HttpDispatcher(self.CONF) + + # The target should be None + self.assertEqual('', dispatcher.target) + + with mock.patch.object(requests, 'post') as post: + dispatcher.record_metering_data(self.msg) + + # Since the target is not set, no http post should occur, thus the + # call_count should be zero. + self.assertEqual(0, post.call_count) + + def test_http_dispatcher_with_no_metadata(self): + self.CONF.dispatcher_http.target = 'fake' + self.CONF.dispatcher_http.cadf_only = True + dispatcher = http.HttpDispatcher(self.CONF) + + with mock.patch.object(requests, 'post') as post: + dispatcher.record_metering_data(self.msg) + + self.assertEqual(0, post.call_count) + + def test_http_dispatcher_without_cadf_event(self): + self.CONF.dispatcher_http.target = 'fake' + self.CONF.dispatcher_http.cadf_only = True + dispatcher = http.HttpDispatcher(self.CONF) + + self.msg['resource_metadata'] = {'request': {'NONE_CADF_EVENT': { + 'q1': 'v1', 'q2': 'v2'}, }, } + self.msg['message_signature'] = utils.compute_signature( + self.msg, + self.CONF.publisher.metering_secret, + ) + + with mock.patch.object(requests, 'post') as post: + dispatcher.record_metering_data(self.msg) + + # Since the meter does not have metadata or CADF_EVENT, the method + # call count should be zero + self.assertEqual(0, post.call_count) + + def test_http_dispatcher_with_cadf_event(self): + self.CONF.dispatcher_http.target = 'fake' + self.CONF.dispatcher_http.cadf_only = True + dispatcher = http.HttpDispatcher(self.CONF) + + self.msg['resource_metadata'] = {'request': {'CADF_EVENT': { + 'q1': 'v1', 'q2': 'v2'}, }, } + self.msg['message_signature'] = utils.compute_signature( + self.msg, + self.CONF.publisher.metering_secret, + ) + + with mock.patch.object(requests, 'post') as post: + dispatcher.record_metering_data(self.msg) + + self.assertEqual(1, post.call_count) + + def test_http_dispatcher_with_none_cadf_event(self): + self.CONF.dispatcher_http.target = 'fake' + self.CONF.dispatcher_http.cadf_only = False + dispatcher = http.HttpDispatcher(self.CONF) + + self.msg['resource_metadata'] = {'any': {'thing1': 'v1', + 'thing2': 'v2', }, } + self.msg['message_signature'] = utils.compute_signature( + self.msg, + self.CONF.publisher.metering_secret, + ) + + with mock.patch.object(requests, 'post') as post: + dispatcher.record_metering_data(self.msg) + + self.assertEqual(1, post.call_count) diff --git a/doc/source/install/manual.rst b/doc/source/install/manual.rst old mode 100644 new mode 100755 index 172af6e23..b4fbc15ef --- a/doc/source/install/manual.rst +++ b/doc/source/install/manual.rst @@ -633,20 +633,29 @@ Ceilometer to send metering data to other systems in addition to the database, multiple dispatchers can be developed and enabled by modifying Ceilometer configuration file. -Ceilometer ships two dispatchers currently. One is called database -dispatcher, and the other is called file dispatcher. As the names imply, -database dispatcher basically sends metering data to a database driver, -eventually metering data will be saved in database. File dispatcher sends -metering data into a file. The location, name, size of the file can be -configured in ceilometer configuration file. These two dispatchers are -shipped in the Ceilometer egg and defined in the entry_points as follows:: +Ceilometer ships multiple dispatchers currently. They are database, file and +http dispatcher. As the names imply, database dispatcher sends metering data +to a database, file dispatcher logs meters into a file, http dispatcher posts +the meters onto a http target. Each dispatcher can have its own configuration +parameters. Please see available configuration parameters at the beginning of +each dispatcher file. + +To check if any of the dispatchers is available in your system, you can +inspect the Ceilometer egg entry_points.txt file, you should normally see text +like the following:: [ceilometer.dispatcher] - file = ceilometer.dispatcher.file:FileDispatcher database = ceilometer.dispatcher.database:DatabaseDispatcher + file = ceilometer.dispatcher.file:FileDispatcher + http = ceilometer.dispatcher.http:HttpDispatcher -To use both dispatchers on a Ceilometer collector service, add the following -line in file ceilometer.conf:: +To configure one or multiple dispatchers for Ceilometer, find the Ceilometer +configuration file ceilometer.conf which is normally located at /etc/ceilometer +directory and make changes accordingly. Your configuration file can be in a +different directory. + +To use multiple dispatchers on a Ceilometer collector service, add multiple +dispatcher lines in ceilometer.conf file like the following:: [DEFAULT] dispatcher=database diff --git a/setup.cfg b/setup.cfg old mode 100644 new mode 100755 index 524efed6a..aaa61bef1 --- a/setup.cfg +++ b/setup.cfg @@ -302,6 +302,7 @@ console_scripts = ceilometer.dispatcher = database = ceilometer.dispatcher.database:DatabaseDispatcher file = ceilometer.dispatcher.file:FileDispatcher + http = ceilometer.dispatcher.http:HttpDispatcher network.statistics.drivers = opendaylight = ceilometer.network.statistics.opendaylight.driver:OpenDayLightDriver