From 191748a403e1e38d6cf643d210a7fd9de3a7fc11 Mon Sep 17 00:00:00 2001 From: Darren Hague <d.hague@sap.com> Date: Wed, 9 Nov 2016 18:47:24 +0000 Subject: [PATCH] Enable Basic and https certificate authentication for http publisher Change-Id: I3735db7a37eff9e822e5180349eb8f86002fd0ef --- ceilometer/publisher/http.py | 59 ++++++++++++++++--- ceilometer/tests/unit/publisher/test_http.py | 21 +++++++ ...isher-authentication-6371c5a9aa8d4c03.yaml | 14 +++++ setup.cfg | 2 + 4 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/http-publisher-authentication-6371c5a9aa8d4c03.yaml diff --git a/ceilometer/publisher/http.py b/ceilometer/publisher/http.py index 27d256bef4..2f3602eb9a 100644 --- a/ceilometer/publisher/http.py +++ b/ceilometer/publisher/http.py @@ -35,9 +35,20 @@ class HttpPublisher(publisher.ConfigPublisherBase): `timeout` and `max_retries` will be set to 5 and 2 respectively. Additional parameters are: - - ssl can be enabled by setting `verify_ssl` + - ssl certificate verification can be disabled by setting `verify_ssl` + to False - batching can be configured by `batch` - connection pool size configured using `poolsize` + - Basic authentication can be configured using the URL authentication + scheme: http://username:password@example.com + - For certificate authentication, `clientcert` and `clientkey` are the + paths to the certificate and key files respectively. `clientkey` is + only required if the clientcert file doesn't already contain the key. + + All of the parameters mentioned above get removed during processing, + with the remaining portion of the URL being used as the actual endpoint. + e.g. https://username:password@example.com/path?verify_ssl=False&q=foo + will result in a call to https://example.com/path?q=foo To use this publisher for samples, add the following section to the /etc/ceilometer/pipeline.yaml file or simply add it to an existing @@ -63,7 +74,6 @@ class HttpPublisher(publisher.ConfigPublisherBase): def __init__(self, conf, parsed_url): super(HttpPublisher, self).__init__(conf, parsed_url) - self.target = parsed_url.geturl() if not parsed_url.hostname: raise ValueError('The hostname of an endpoint for ' @@ -83,12 +93,32 @@ class HttpPublisher(publisher.ConfigPublisherBase): self.poster = ( self._do_post if strutils.bool_from_string(self._get_param( params, 'batch', True)) else self._individual_post) + verify_ssl = self._get_param(params, 'verify_ssl', True) try: - self.verify_ssl = strutils.bool_from_string( - self._get_param(params, 'verify_ssl', None), strict=True) + self.verify_ssl = strutils.bool_from_string(verify_ssl, + strict=True) except ValueError: - self.verify_ssl = (self._get_param(params, 'verify_ssl', None) - or True) + self.verify_ssl = (verify_ssl or True) + + username = parsed_url.username + password = parsed_url.password + if username: + self.client_auth = (username, password) + netloc = parsed_url.netloc.replace(username+':'+password+'@', '') + else: + self.client_auth = None + netloc = parsed_url.netloc + + clientcert = self._get_param(params, 'clientcert', None) + clientkey = self._get_param(params, 'clientkey', None) + if clientcert: + if clientkey: + self.client_cert = (clientcert, clientkey) + else: + self.client_cert = clientcert + else: + self.client_cert = None + self.raw_only = strutils.bool_from_string( self._get_param(params, 'raw_only', False)) @@ -96,7 +126,16 @@ class HttpPublisher(publisher.ConfigPublisherBase): kwargs = {'max_retries': self.max_retries, 'pool_connections': pool_size, 'pool_maxsize': pool_size} self.session = requests.Session() - # FIXME(gordc): support https in addition to http + + # authentication & config params have been removed, so use URL with + # updated query string + self.target = urlparse.urlunsplit([ + parsed_url.scheme, + netloc, + parsed_url.path, + urlparse.urlencode(params), + parsed_url.fragment]) + self.session.mount(self.target, adapters.HTTPAdapter(**kwargs)) LOG.debug('HttpPublisher for endpoint %s is initialized!' % @@ -105,8 +144,8 @@ class HttpPublisher(publisher.ConfigPublisherBase): @staticmethod def _get_param(params, name, default_value, cast=None): try: - return cast(params.get(name)[-1]) if cast else params.get(name)[-1] - except (ValueError, TypeError): + return cast(params.pop(name)[-1]) if cast else params.pop(name)[-1] + except (ValueError, TypeError, KeyError): LOG.debug('Default value %(value)s is used for %(name)s' % {'value': default_value, 'name': name}) return default_value @@ -124,6 +163,8 @@ class HttpPublisher(publisher.ConfigPublisherBase): try: res = self.session.post(self.target, data=data, headers=self.headers, timeout=self.timeout, + auth=self.client_auth, + cert=self.client_cert, verify=self.verify_ssl) res.raise_for_status() LOG.debug('Message posting to %s: status code %d.', diff --git a/ceilometer/tests/unit/publisher/test_http.py b/ceilometer/tests/unit/publisher/test_http.py index 9ea49d7275..c340bc36c8 100644 --- a/ceilometer/tests/unit/publisher/test_http.py +++ b/ceilometer/tests/unit/publisher/test_http.py @@ -232,6 +232,27 @@ class TestHttpPublisher(base.BaseTestCase): publisher.publish_samples(self.sample_data) self.assertEqual('/path/to/cert.crt', post.call_args[1]['verify']) + def test_post_basic_auth(self): + parsed_url = urlparse.urlparse( + 'http://alice:l00kingGla$$@localhost:90/path1?') + publisher = http.HttpPublisher(self.CONF, parsed_url) + + with mock.patch.object(requests.Session, 'post') as post: + publisher.publish_samples(self.sample_data) + self.assertEqual(('alice', 'l00kingGla$$'), + post.call_args[1]['auth']) + + def test_post_client_cert_auth(self): + parsed_url = urlparse.urlparse('http://localhost:90/path1?' + 'clientcert=/path/to/cert.crt&' + 'clientkey=/path/to/cert.key') + publisher = http.HttpPublisher(self.CONF, parsed_url) + + with mock.patch.object(requests.Session, 'post') as post: + publisher.publish_samples(self.sample_data) + self.assertEqual(('/path/to/cert.crt', '/path/to/cert.key'), + post.call_args[1]['cert']) + def test_post_raw_only(self): parsed_url = urlparse.urlparse('http://localhost:90/path1?raw_only=1') publisher = http.HttpPublisher(self.CONF, parsed_url) diff --git a/releasenotes/notes/http-publisher-authentication-6371c5a9aa8d4c03.yaml b/releasenotes/notes/http-publisher-authentication-6371c5a9aa8d4c03.yaml new file mode 100644 index 0000000000..807ea6dd23 --- /dev/null +++ b/releasenotes/notes/http-publisher-authentication-6371c5a9aa8d4c03.yaml @@ -0,0 +1,14 @@ +--- +features: + - In the 'publishers' section of a meter/event pipeline definition, https:// + can now be used in addition to http://. Furthermore, either Basic or + client-certificate authentication can be used (obviously, client cert only + makes sense in the https case). + For Basic authentication, use the form http://username:password@hostname/. + For client certificate authentication pass the client certificate's path + (and the key file path, if the key is not in the certificate file) using + the parameters 'clientcert' and 'clientkey', e.g. + https://hostname/path?clientcert=/path/to/cert&clientkey=/path/to/key. + Any parameters or credentials used for http(s) publishers are removed from + the URL before the actual HTTP request is made. + diff --git a/setup.cfg b/setup.cfg index 22a15bd84f..092a5f4667 100644 --- a/setup.cfg +++ b/setup.cfg @@ -231,6 +231,7 @@ ceilometer.sample.publisher = direct = ceilometer.publisher.direct:DirectPublisher kafka = ceilometer.publisher.kafka_broker:KafkaBrokerPublisher http = ceilometer.publisher.http:HttpPublisher + https = ceilometer.publisher.http:HttpPublisher gnocchi = ceilometer.publisher.direct:DirectPublisher database = ceilometer.publisher.direct:DirectPublisher file_alt = ceilometer.publisher.direct:DirectPublisher @@ -242,6 +243,7 @@ ceilometer.event.publisher = notifier = ceilometer.publisher.messaging:EventNotifierPublisher kafka = ceilometer.publisher.kafka_broker:KafkaBrokerPublisher http = ceilometer.publisher.http:HttpPublisher + https = ceilometer.publisher.http:HttpPublisher gnocchi = ceilometer.publisher.direct:DirectPublisher database = ceilometer.publisher.direct:DirectPublisher file_alt = ceilometer.publisher.direct:DirectPublisher