1 From: Sylvain Afchain <sylvain.afchain@enovance.com>
2 Date: Sat, 5 Apr 2014 05:43:55 +0000 (+0200)
3 Subject: Opencontrail network statistics driver
4 X-Git-Url: https://review.openstack.org/gitweb?p=openstack%2Fceilometer.git;a=commitdiff_plain;h=891819736dcbd04b9ca81245419f87dadb237b97
6 Opencontrail network statistics driver
8 This patch introduces a network statistics driver
9 for Opencontrail. Only port statistics are currently
10 returned by the driver.
12 Implements: blueprint meter-from-opencontrail
13 Co-Authored-By: Edouard Thuleau <edouard.thuleau@cloudwatt.com>
14 Change-Id: Ic0afc478362fb4170903ee4e3723b82cd6c723fa
15 (cherry picked from commit 6e0f4d9bd9c7f3b957adc6f73bf1a48c8c120e1b)
18 diff --git a/ceilometer/network/statistics/opencontrail/__init__.py b/ceilometer/network/statistics/opencontrail/__init__.py
20 index 0000000..e69de29
21 diff --git a/ceilometer/network/statistics/opencontrail/client.py b/ceilometer/network/statistics/opencontrail/client.py
23 index 0000000..51f786d
25 +++ b/ceilometer/network/statistics/opencontrail/client.py
27 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
29 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
31 +# Licensed under the Apache License, Version 2.0 (the "License"); you may
32 +# not use this file except in compliance with the License. You may obtain
33 +# a copy of the License at
35 +# http://www.apache.org/licenses/LICENSE-2.0
37 +# Unless required by applicable law or agreed to in writing, software
38 +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
39 +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
40 +# License for the specific language governing permissions and limitations
43 +from oslo.config import cfg
46 +from six.moves.urllib import parse as url_parse
48 +from ceilometer.openstack.common.gettextutils import _ # noqa
49 +from ceilometer.openstack.common import log
55 +LOG = log.getLogger(__name__)
58 +class OpencontrailAPIFailed(Exception):
62 +class AnalyticsAPIBaseClient(object):
63 + """Opencontrail Base Statistics REST API Client."""
65 + def __init__(self, endpoint, username, password, domain, verify_ssl=True):
66 + self.endpoint = endpoint
67 + self.username = username
68 + self.password = password
69 + self.domain = domain
70 + self.verify_ssl = verify_ssl
73 + def authenticate(self):
74 + path = '/authenticate'
75 + data = {'username': self.username,
76 + 'password': self.password,
77 + 'domain': self.domain}
79 + req_params = self._get_req_params(data=data)
80 + url = url_parse.urljoin(self.endpoint, path)
81 + resp = requests.post(url, **req_params)
82 + if resp.status_code != 302:
83 + raise OpencontrailAPIFailed(
84 + _('Opencontrail API returned %(status)s %(reason)s') %
85 + {'status': resp.status_code, 'reason': resp.reason})
86 + self.sid = resp.cookies['connect.sid']
88 + def request(self, path, fqdn_uuid, data, retry=True):
93 + data = {'fqnUUID': fqdn_uuid}
95 + data['fqnUUID'] = fqdn_uuid
97 + req_params = self._get_req_params(data=data,
98 + cookies={'connect.sid': self.sid})
100 + url = url_parse.urljoin(self.endpoint, path)
101 + self._log_req(url, req_params)
102 + resp = requests.get(url, **req_params)
103 + self._log_res(resp)
105 + # it seems that the sid token has to be renewed
106 + if resp.status_code == 302:
109 + return self.request(path, fqdn_uuid, data,
112 + if resp.status_code != 200:
113 + raise OpencontrailAPIFailed(
114 + _('Opencontrail API returned %(status)s %(reason)s') %
115 + {'status': resp.status_code, 'reason': resp.reason})
119 + def _get_req_params(self, params=None, data=None, cookies=None):
122 + 'Accept': 'application/json'
125 + 'verify': self.verify_ssl,
126 + 'allow_redirects': False,
133 + def _log_req(url, req_params):
137 + curl_command = ['REQ: curl -i -X GET ']
140 + for name, value in six.iteritems(req_params['data']):
141 + params.append("%s=%s" % (name, value))
143 + curl_command.append('"%s?%s" ' % (url, '&'.join(params)))
145 + for name, value in six.iteritems(req_params['headers']):
146 + curl_command.append('-H "%s: %s" ' % (name, value))
148 + LOG.debug(''.join(curl_command))
151 + def _log_res(resp):
156 + dump.append('HTTP %.1f %s %s\n' % (resp.raw.version,
159 + dump.extend(['%s: %s\n' % (k, v)
160 + for k, v in six.iteritems(resp.headers)])
163 + dump.extend([resp.content, '\n'])
165 + LOG.debug(''.join(dump))
168 +class NetworksAPIClient(AnalyticsAPIBaseClient):
169 + """Opencontrail Statistics REST API Client."""
171 + def get_port_statistics(self, fqdn_uuid):
172 + """Get port statistics of a network
175 + /tenant/networking/virtual-machines/details
181 + path = '/api/tenant/networking/virtual-machines/details'
182 + resp = self.request(path, fqdn_uuid, {'type': 'vn'})
187 +class Client(object):
189 + def __init__(self, endpoint, username, password, domain, verify_ssl=True):
190 + self.networks = NetworksAPIClient(endpoint, username, password,
191 + domain, verify_ssl)
192 diff --git a/ceilometer/network/statistics/opencontrail/driver.py b/ceilometer/network/statistics/opencontrail/driver.py
194 index 0000000..a54de7d
196 +++ b/ceilometer/network/statistics/opencontrail/driver.py
198 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
200 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
202 +# Licensed under the Apache License, Version 2.0 (the "License"); you may
203 +# not use this file except in compliance with the License. You may obtain
204 +# a copy of the License at
206 +# http://www.apache.org/licenses/LICENSE-2.0
208 +# Unless required by applicable law or agreed to in writing, software
209 +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
210 +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
211 +# License for the specific language governing permissions and limitations
212 +# under the License.
214 +from six.moves.urllib import parse as url_parse
216 +from ceilometer.network.statistics import driver
217 +from ceilometer.network.statistics.opencontrail import client
218 +from ceilometer import neutron_client
219 +from ceilometer.openstack.common import timeutils
222 +class OpencontrailDriver(driver.Driver):
223 + """Driver of network analytics of Opencontrail.
225 + This driver uses resources in "pipeline.yaml".
226 + Resource requires below conditions:
228 + * scheme is "opencontrail"
230 + This driver can be configured via query parameters.
231 + Supported parameters:
233 + The scheme of request url to Opencontrail Analytics endpoint.
236 + This is username used by Opencontrail Analytics.(default None)
238 + This is password used by Opencontrail Analytics.(default None)
240 + This is domain used by Opencontrail Analytics.(default None)
242 + Specify if the certificate will be checked for https request.
246 + opencontrail://localhost:8143/?username=admin&password=admin&
247 + scheme=https&domain=&verify_ssl=true
250 + def _prepare_cache(endpoint, params, cache):
252 + if 'network.statistics.opencontrail' in cache:
253 + return cache['network.statistics.opencontrail']
256 + 'o_client': client.Client(endpoint,
257 + params['username'],
258 + params['password'],
259 + params.get('domain'),
260 + params.get('verify_ssl') == 'true'),
261 + 'n_client': neutron_client.Client()
264 + cache['network.statistics.opencontrail'] = data
268 + def get_sample_data(self, meter_name, parse_url, params, cache):
270 + parts = url_parse.ParseResult(params.get('scheme', ['http'])[0],
276 + endpoint = url_parse.urlunparse(parts)
278 + iter = self._get_iter(meter_name)
280 + # The extractor for this meter is not implemented or the API
281 + # doesn't have method to get this meter.
284 + extractor = self._get_extractor(meter_name)
285 + if extractor is None:
286 + # The extractor for this meter is not implemented or the API
287 + # doesn't have method to get this meter.
290 + data = self._prepare_cache(endpoint, params, cache)
292 + ports = data['n_client'].port_get_all()
293 + ports_map = dict((port['id'], port['tenant_id']) for port in ports)
295 + networks = data['n_client'].network_get_all()
297 + for network in networks:
298 + net_id = network['id']
300 + timestamp = timeutils.utcnow().isoformat()
301 + statistics = data['o_client'].networks.get_port_statistics(net_id)
305 + for value in statistics['value']:
306 + for sample in iter(extractor, value, ports_map):
307 + if sample is not None:
308 + sample[2]['network_id'] = net_id
309 + yield sample + (timestamp, )
311 + def _get_iter(self, meter_name):
312 + if meter_name.startswith('switch.port'):
313 + return self._iter_port
315 + def _get_extractor(self, meter_name):
316 + method_name = '_' + meter_name.replace('.', '_')
317 + return getattr(self, method_name, None)
320 + def _iter_port(extractor, value, ports_map):
321 + ifstats = value['value']['UveVirtualMachineAgent']['if_stats_list']
322 + for ifstat in ifstats:
323 + name = ifstat['name']
324 + device_owner_id, port_id = name.split(':')
326 + tenant_id = ports_map.get(port_id)
328 + resource_meta = {'device_owner_id': device_owner_id,
329 + 'tenant_id': tenant_id}
330 + yield extractor(ifstat, port_id, resource_meta)
333 + def _switch_port_receive_packets(statistic, resource_id, resource_meta):
334 + return (int(statistic['in_pkts']), resource_id, resource_meta)
337 + def _switch_port_transmit_packets(statistic, resource_id, resource_meta):
338 + return (int(statistic['out_pkts']), resource_id, resource_meta)
341 + def _switch_port_receive_bytes(statistic, resource_id, resource_meta):
342 + return (int(statistic['in_bytes']), resource_id, resource_meta)
345 + def _switch_port_transmit_bytes(statistic, resource_id, resource_meta):
346 + return (int(statistic['out_bytes']), resource_id, resource_meta)
347 diff --git a/ceilometer/neutron_client.py b/ceilometer/neutron_client.py
349 index 0000000..993df58
351 +++ b/ceilometer/neutron_client.py
353 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
355 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
357 +# Licensed under the Apache License, Version 2.0 (the "License"); you may
358 +# not use this file except in compliance with the License. You may obtain
359 +# a copy of the License at
361 +# http://www.apache.org/licenses/LICENSE-2.0
363 +# Unless required by applicable law or agreed to in writing, software
364 +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
365 +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
366 +# License for the specific language governing permissions and limitations
367 +# under the License.
371 +from neutronclient.v2_0 import client as clientv20
372 +from oslo.config import cfg
374 +from ceilometer.openstack.common import log
376 +cfg.CONF.import_group('service_credentials', 'ceilometer.service')
378 +LOG = log.getLogger(__name__)
383 + @functools.wraps(func)
384 + def with_logging(*args, **kwargs):
386 + return func(*args, **kwargs)
387 + except Exception as e:
391 + return with_logging
394 +class Client(object):
395 + """A client which gets information via python-neutronclient."""
397 + def __init__(self):
398 + conf = cfg.CONF.service_credentials
400 + 'insecure': conf.insecure,
401 + 'ca_cert': conf.os_cacert,
402 + 'username': conf.os_username,
403 + 'password': conf.os_password,
404 + 'auth_url': conf.os_auth_url,
405 + 'region_name': conf.os_region_name,
406 + 'endpoint_type': conf.os_endpoint_type
409 + if conf.os_tenant_id:
410 + params['tenant_id'] = conf.os_tenant_id
412 + params['tenant_name'] = conf.os_tenant_name
414 + self.client = clientv20.Client(**params)
417 + def network_get_all(self):
418 + """Returns all networks."""
419 + resp = self.client.list_networks()
420 + return resp.get('networks')
423 + def port_get_all(self):
424 + resp = self.client.list_ports()
425 + return resp.get('ports')
426 diff --git a/ceilometer/tests/network/statistics/opencontrail/__init__.py b/ceilometer/tests/network/statistics/opencontrail/__init__.py
428 index 0000000..e69de29
429 diff --git a/ceilometer/tests/network/statistics/opencontrail/test_client.py b/ceilometer/tests/network/statistics/opencontrail/test_client.py
431 index 0000000..1817a32
433 +++ b/ceilometer/tests/network/statistics/opencontrail/test_client.py
435 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
437 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
439 +# Licensed under the Apache License, Version 2.0 (the "License"); you may
440 +# not use this file except in compliance with the License. You may obtain
441 +# a copy of the License at
443 +# http://www.apache.org/licenses/LICENSE-2.0
445 +# Unless required by applicable law or agreed to in writing, software
446 +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
447 +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
448 +# License for the specific language governing permissions and limitations
449 +# under the License.
453 +from ceilometer.network.statistics.opencontrail import client
454 +from ceilometer.openstack.common import test
457 +class TestOpencontrailClient(test.BaseTestCase):
460 + super(TestOpencontrailClient, self).setUp()
461 + self.client = client.Client('http://127.0.0.1:8143',
462 + 'admin', 'admin', None, False)
464 + self.post_resp = mock.MagicMock()
465 + self.post = mock.patch('requests.post',
466 + return_value=self.post_resp).start()
468 + self.post_resp.raw.version = 1.1
469 + self.post_resp.status_code = 302
470 + self.post_resp.reason = 'Moved'
471 + self.post_resp.headers = {}
472 + self.post_resp.cookies = {'connect.sid': 'aaa'}
473 + self.post_resp.content = 'dummy'
475 + self.get_resp = mock.MagicMock()
476 + self.get = mock.patch('requests.get',
477 + return_value=self.get_resp).start()
478 + self.get_resp.raw_version = 1.1
479 + self.get_resp.status_code = 200
480 + self.post_resp.content = 'dqs'
482 + def test_port_statistics(self):
484 + self.client.networks.get_port_statistics(uuid)
486 + call_args = self.post.call_args_list[0][0]
487 + call_kwargs = self.post.call_args_list[0][1]
489 + expected_url = 'http://127.0.0.1:8143/authenticate'
490 + self.assertEqual(expected_url, call_args[0])
492 + data = call_kwargs.get('data')
493 + expected_data = {'domain': None, 'password': 'admin',
494 + 'username': 'admin'}
495 + self.assertEqual(expected_data, data)
497 + call_args = self.get.call_args_list[0][0]
498 + call_kwargs = self.get.call_args_list[0][1]
500 + expected_url = ('http://127.0.0.1:8143/api/tenant/'
501 + 'networking/virtual-machines/details')
502 + self.assertEqual(expected_url, call_args[0])
504 + data = call_kwargs.get('data')
505 + cookies = call_kwargs.get('cookies')
507 + expected_data = {'fqnUUID': 'bbb', 'type': 'vn'}
508 + expected_cookies = {'connect.sid': 'aaa'}
509 + self.assertEqual(expected_data, data)
510 + self.assertEqual(expected_cookies, cookies)
511 diff --git a/ceilometer/tests/network/statistics/opencontrail/test_driver.py b/ceilometer/tests/network/statistics/opencontrail/test_driver.py
513 index 0000000..940e998
515 +++ b/ceilometer/tests/network/statistics/opencontrail/test_driver.py
517 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
519 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
521 +# Licensed under the Apache License, Version 2.0 (the "License"); you may
522 +# not use this file except in compliance with the License. You may obtain
523 +# a copy of the License at
525 +# http://www.apache.org/licenses/LICENSE-2.0
527 +# Unless required by applicable law or agreed to in writing, software
528 +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
529 +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
530 +# License for the specific language governing permissions and limitations
531 +# under the License.
534 +from six.moves.urllib import parse as url_parse
536 +from ceilometer.network.statistics.opencontrail import driver
537 +from ceilometer.openstack.common import test
540 +class TestOpencontrailDriver(test.BaseTestCase):
543 + super(TestOpencontrailDriver, self).setUp()
545 + self.nc_ports = mock.patch('ceilometer.neutron_client'
546 + '.Client.port_get_all',
547 + return_value=self.fake_ports())
548 + self.nc_ports.start()
550 + self.nc_networks = mock.patch('ceilometer.neutron_client'
551 + '.Client.network_get_all',
552 + return_value=self.fake_networks())
553 + self.nc_networks.start()
555 + self.driver = driver.OpencontrailDriver()
556 + self.parse_url = url_parse.ParseResult('opencontrail',
558 + '/', None, None, None)
559 + self.params = {'password': ['admin'],
560 + 'scheme': ['http'],
561 + 'username': ['admin'],
562 + 'verify_ssl': ['false']}
566 + return [{'admin_state_up': True,
567 + 'device_owner': 'compute:None',
568 + 'device_id': '674e553b-8df9-4321-87d9-93ba05b93558',
569 + 'extra_dhcp_opts': [],
570 + 'id': '96d49cc3-4e01-40ce-9cac-c0e32642a442',
571 + 'mac_address': 'fa:16:3e:c5:35:93',
573 + 'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
574 + 'status': 'ACTIVE',
575 + 'tenant_id': '89271fa581ab4380bf172f868c3615f9'}]
578 + def fake_networks():
579 + return [{'admin_state_up': True,
580 + 'id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
582 + 'provider:network_type': 'gre',
583 + 'provider:physical_network': None,
584 + 'provider:segmentation_id': 2,
585 + 'router:external': True,
587 + 'status': 'ACTIVE',
588 + 'subnets': [u'c4b6f5b8-3508-4896-b238-a441f25fb492'],
589 + 'tenant_id': '62d6f08bbd3a44f6ad6f00ca15cce4e5'}]
592 + def fake_port_stats():
593 + return {"value": [{
594 + "name": "c588ebb7-ae52-485a-9f0c-b2791c5da196",
596 + "UveVirtualMachineAgent": {
597 + "if_stats_list": [{
599 + "in_bandwidth_usage": 0,
601 + "out_bandwidth_usage": 0,
604 + "name": ("674e553b-8df9-4321-87d9-93ba05b93558:"
605 + "96d49cc3-4e01-40ce-9cac-c0e32642a442")
608 + def _test_meter(self, meter_name, expected):
609 + with mock.patch('ceilometer.network.'
610 + 'statistics.opencontrail.'
611 + 'client.NetworksAPIClient.'
612 + 'get_port_statistics',
613 + return_value=self.fake_port_stats()) as port_stats:
615 + samples = self.driver.get_sample_data(meter_name, self.parse_url,
618 + self.assertEqual(expected, [s for s in samples])
620 + net_id = '298a3088-a446-4d5a-bad8-f92ecacd786b'
621 + port_stats.assert_called_with(net_id)
623 + def test_switch_port_receive_packets(self):
626 + '96d49cc3-4e01-40ce-9cac-c0e32642a442',
627 + {'device_owner_id': '674e553b-8df9-4321-87d9-93ba05b93558',
628 + 'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
629 + 'tenant_id': '89271fa581ab4380bf172f868c3615f9'},
631 + self._test_meter('switch.port.receive.packets', expected)
633 + def test_switch_port_transmit_packets(self):
636 + '96d49cc3-4e01-40ce-9cac-c0e32642a442',
637 + {'device_owner_id': '674e553b-8df9-4321-87d9-93ba05b93558',
638 + 'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
639 + 'tenant_id': '89271fa581ab4380bf172f868c3615f9'},
641 + self._test_meter('switch.port.transmit.packets', expected)
643 + def test_switch_port_receive_bytes(self):
646 + '96d49cc3-4e01-40ce-9cac-c0e32642a442',
647 + {'device_owner_id': '674e553b-8df9-4321-87d9-93ba05b93558',
648 + 'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
649 + 'tenant_id': '89271fa581ab4380bf172f868c3615f9'},
651 + self._test_meter('switch.port.receive.bytes', expected)
653 + def test_switch_port_transmit_bytes(self):
656 + '96d49cc3-4e01-40ce-9cac-c0e32642a442',
657 + {'device_owner_id': '674e553b-8df9-4321-87d9-93ba05b93558',
658 + 'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
659 + 'tenant_id': '89271fa581ab4380bf172f868c3615f9'},
661 + self._test_meter('switch.port.transmit.bytes', expected)
662 diff --git a/ceilometer/tests/test_neutronclient.py b/ceilometer/tests/test_neutronclient.py
664 index 0000000..17d39b6
666 +++ b/ceilometer/tests/test_neutronclient.py
668 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
670 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
672 +# Licensed under the Apache License, Version 2.0 (the "License"); you may
673 +# not use this file except in compliance with the License. You may obtain
674 +# a copy of the License at
676 +# http://www.apache.org/licenses/LICENSE-2.0
678 +# Unless required by applicable law or agreed to in writing, software
679 +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
680 +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
681 +# License for the specific language governing permissions and limitations
682 +# under the License.
684 +from mock import patch
686 +from ceilometer import neutron_client
687 +from ceilometer.openstack.common import test
690 +class TestNeutronClient(test.BaseTestCase):
693 + super(TestNeutronClient, self).setUp()
694 + self.nc = neutron_client.Client()
697 + def fake_ports_list():
699 + [{'admin_state_up': True,
700 + 'device_id': '674e553b-8df9-4321-87d9-93ba05b93558',
701 + 'device_owner': 'network:router_gateway',
702 + 'extra_dhcp_opts': [],
703 + 'id': '96d49cc3-4e01-40ce-9cac-c0e32642a442',
704 + 'mac_address': 'fa:16:3e:c5:35:93',
706 + 'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
707 + 'status': 'ACTIVE',
708 + 'tenant_id': '89271fa581ab4380bf172f868c3615f9'}]}
710 + def test_port_get_all(self):
711 + with patch.object(self.nc.client, 'list_ports',
712 + side_effect=self.fake_ports_list):
713 + ports = self.nc.port_get_all()
715 + self.assertEqual(1, len(ports))
716 + self.assertEqual('96d49cc3-4e01-40ce-9cac-c0e32642a442',
720 + def fake_networks_list():
721 + return {'networks':
722 + [{'admin_state_up': True,
723 + 'id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
725 + 'provider:network_type': 'gre',
726 + 'provider:physical_network': None,
727 + 'provider:segmentation_id': 2,
728 + 'router:external': True,
730 + 'status': 'ACTIVE',
731 + 'subnets': [u'c4b6f5b8-3508-4896-b238-a441f25fb492'],
732 + 'tenant_id': '62d6f08bbd3a44f6ad6f00ca15cce4e5'}]}
734 + def test_network_get_all(self):
735 + with patch.object(self.nc.client, 'list_networks',
736 + side_effect=self.fake_networks_list):
737 + networks = self.nc.network_get_all()
739 + self.assertEqual(1, len(networks))
740 + self.assertEqual('298a3088-a446-4d5a-bad8-f92ecacd786b',
742 diff --git a/requirements.txt b/requirements.txt
743 index 654f568..d325ea8 100644
744 --- a/requirements.txt
745 +++ b/requirements.txt
746 @@ -23,6 +23,7 @@ python-glanceclient>=0.9.0
747 python-keystoneclient>=0.7.0
748 python-novaclient>=2.17.0
749 python-swiftclient>=1.6
750 +python-neutronclient>=2.3.4,<3
754 diff --git a/setup.cfg b/setup.cfg
755 index 8cd49ed..b7ce034 100644
758 @@ -200,6 +200,7 @@ ceilometer.dispatcher =
760 network.statistics.drivers =
761 opendaylight = ceilometer.network.statistics.opendaylight.driver:OpenDayLightDriver
762 + opencontrail = ceilometer.network.statistics.opencontrail.driver:OpencontrailDriver