Now generating the ceilometer.conf on the fly, since it's gone from upstream.
[openstack-build/ceilometer-build.git] / trusty / debian / patches / Opencontrail_network_statistics_driver.patch
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
5
6 Opencontrail network statistics driver
7
8 This patch introduces a network statistics driver
9 for Opencontrail. Only port statistics are currently
10 returned by the driver.
11
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)
16 ---
17
18 diff --git a/ceilometer/network/statistics/opencontrail/__init__.py b/ceilometer/network/statistics/opencontrail/__init__.py
19 new file mode 100644
20 index 0000000..e69de29
21 diff --git a/ceilometer/network/statistics/opencontrail/client.py b/ceilometer/network/statistics/opencontrail/client.py
22 new file mode 100644
23 index 0000000..51f786d
24 --- /dev/null
25 +++ b/ceilometer/network/statistics/opencontrail/client.py
26 @@ -0,0 +1,165 @@
27 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
28 +#
29 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
30 +#
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
34 +#
35 +#      http://www.apache.org/licenses/LICENSE-2.0
36 +#
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
41 +# under the License.
42 +
43 +from oslo.config import cfg
44 +import requests
45 +import six
46 +from six.moves.urllib import parse as url_parse
47 +
48 +from ceilometer.openstack.common.gettextutils import _  # noqa
49 +from ceilometer.openstack.common import log
50 +
51 +
52 +CONF = cfg.CONF
53 +
54 +
55 +LOG = log.getLogger(__name__)
56 +
57 +
58 +class OpencontrailAPIFailed(Exception):
59 +    pass
60 +
61 +
62 +class AnalyticsAPIBaseClient(object):
63 +    """Opencontrail Base Statistics REST API Client."""
64 +
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
71 +        self.sid = None
72 +
73 +    def authenticate(self):
74 +        path = '/authenticate'
75 +        data = {'username': self.username,
76 +                'password': self.password,
77 +                'domain': self.domain}
78 +
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']
87 +
88 +    def request(self, path, fqdn_uuid, data, retry=True):
89 +        if not self.sid:
90 +            self.authenticate()
91 +
92 +        if not data:
93 +            data = {'fqnUUID': fqdn_uuid}
94 +        else:
95 +            data['fqnUUID'] = fqdn_uuid
96 +
97 +        req_params = self._get_req_params(data=data,
98 +                                          cookies={'connect.sid': self.sid})
99 +
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)
104 +
105 +        # it seems that the sid token has to be renewed
106 +        if resp.status_code == 302:
107 +            self.sid = 0
108 +            if retry:
109 +                return self.request(path, fqdn_uuid, data,
110 +                                    retry=False)
111 +
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})
116 +
117 +        return resp
118 +
119 +    def _get_req_params(self, params=None, data=None, cookies=None):
120 +        req_params = {
121 +            'headers': {
122 +                'Accept': 'application/json'
123 +            },
124 +            'data': data,
125 +            'verify': self.verify_ssl,
126 +            'allow_redirects': False,
127 +            'cookies': cookies
128 +        }
129 +
130 +        return req_params
131 +
132 +    @staticmethod
133 +    def _log_req(url, req_params):
134 +        if not CONF.debug:
135 +            return
136 +
137 +        curl_command = ['REQ: curl -i -X GET ']
138 +
139 +        params = []
140 +        for name, value in six.iteritems(req_params['data']):
141 +            params.append("%s=%s" % (name, value))
142 +
143 +        curl_command.append('"%s?%s" ' % (url, '&'.join(params)))
144 +
145 +        for name, value in six.iteritems(req_params['headers']):
146 +            curl_command.append('-H "%s: %s" ' % (name, value))
147 +
148 +        LOG.debug(''.join(curl_command))
149 +
150 +    @staticmethod
151 +    def _log_res(resp):
152 +        if not CONF.debug:
153 +            return
154 +
155 +        dump = ['RES: \n']
156 +        dump.append('HTTP %.1f %s %s\n' % (resp.raw.version,
157 +                                           resp.status_code,
158 +                                           resp.reason))
159 +        dump.extend(['%s: %s\n' % (k, v)
160 +                     for k, v in six.iteritems(resp.headers)])
161 +        dump.append('\n')
162 +        if resp.content:
163 +            dump.extend([resp.content, '\n'])
164 +
165 +        LOG.debug(''.join(dump))
166 +
167 +
168 +class NetworksAPIClient(AnalyticsAPIBaseClient):
169 +    """Opencontrail Statistics REST API Client."""
170 +
171 +    def get_port_statistics(self, fqdn_uuid):
172 +        """Get port statistics of a network
173 +
174 +        URL:
175 +            /tenant/networking/virtual-machines/details
176 +        PARAMS:
177 +            fqdnUUID=fqdn_uuid
178 +            type=vn
179 +        """
180 +
181 +        path = '/api/tenant/networking/virtual-machines/details'
182 +        resp = self.request(path, fqdn_uuid, {'type': 'vn'})
183 +
184 +        return resp.json()
185 +
186 +
187 +class Client(object):
188 +
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
193 new file mode 100644
194 index 0000000..a54de7d
195 --- /dev/null
196 +++ b/ceilometer/network/statistics/opencontrail/driver.py
197 @@ -0,0 +1,149 @@
198 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
199 +#
200 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
201 +#
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
205 +#
206 +#      http://www.apache.org/licenses/LICENSE-2.0
207 +#
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.
213 +
214 +from six.moves.urllib import parse as url_parse
215 +
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
220 +
221 +
222 +class OpencontrailDriver(driver.Driver):
223 +    """Driver of network analytics of Opencontrail.
224 +
225 +    This driver uses resources in "pipeline.yaml".
226 +    Resource requires below conditions:
227 +    * resource is url
228 +    * scheme is "opencontrail"
229 +
230 +    This driver can be configured via query parameters.
231 +    Supported parameters:
232 +    * scheme:
233 +        The scheme of request url to Opencontrail Analytics endpoint.
234 +        (default http)
235 +    * username:
236 +        This is username used by Opencontrail Analytics.(default None)
237 +    * password:
238 +        This is password used by Opencontrail Analytics.(default None)
239 +    * domain
240 +        This is domain used by Opencontrail Analytics.(default None)
241 +    * verify_ssl
242 +        Specify if the certificate will be checked for https request.
243 +        (default false)
244 +
245 +    e.g.
246 +        opencontrail://localhost:8143/?username=admin&password=admin&
247 +        scheme=https&domain=&verify_ssl=true
248 +    """
249 +    @staticmethod
250 +    def _prepare_cache(endpoint, params, cache):
251 +
252 +        if 'network.statistics.opencontrail' in cache:
253 +            return cache['network.statistics.opencontrail']
254 +
255 +        data = {
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()
262 +        }
263 +
264 +        cache['network.statistics.opencontrail'] = data
265 +
266 +        return data
267 +
268 +    def get_sample_data(self, meter_name, parse_url, params, cache):
269 +
270 +        parts = url_parse.ParseResult(params.get('scheme', ['http'])[0],
271 +                                      parse_url.netloc,
272 +                                      parse_url.path,
273 +                                      None,
274 +                                      None,
275 +                                      None)
276 +        endpoint = url_parse.urlunparse(parts)
277 +
278 +        iter = self._get_iter(meter_name)
279 +        if iter is None:
280 +            # The extractor for this meter is not implemented or the API
281 +            # doesn't have method to get this meter.
282 +            return
283 +
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.
288 +            return
289 +
290 +        data = self._prepare_cache(endpoint, params, cache)
291 +
292 +        ports = data['n_client'].port_get_all()
293 +        ports_map = dict((port['id'], port['tenant_id']) for port in ports)
294 +
295 +        networks = data['n_client'].network_get_all()
296 +
297 +        for network in networks:
298 +            net_id = network['id']
299 +
300 +            timestamp = timeutils.utcnow().isoformat()
301 +            statistics = data['o_client'].networks.get_port_statistics(net_id)
302 +            if not statistics:
303 +                continue
304 +
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, )
310 +
311 +    def _get_iter(self, meter_name):
312 +        if meter_name.startswith('switch.port'):
313 +            return self._iter_port
314 +
315 +    def _get_extractor(self, meter_name):
316 +        method_name = '_' + meter_name.replace('.', '_')
317 +        return getattr(self, method_name, None)
318 +
319 +    @staticmethod
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(':')
325 +
326 +            tenant_id = ports_map.get(port_id)
327 +
328 +            resource_meta = {'device_owner_id': device_owner_id,
329 +                             'tenant_id': tenant_id}
330 +            yield extractor(ifstat, port_id, resource_meta)
331 +
332 +    @staticmethod
333 +    def _switch_port_receive_packets(statistic, resource_id, resource_meta):
334 +        return (int(statistic['in_pkts']), resource_id, resource_meta)
335 +
336 +    @staticmethod
337 +    def _switch_port_transmit_packets(statistic, resource_id, resource_meta):
338 +        return (int(statistic['out_pkts']), resource_id, resource_meta)
339 +
340 +    @staticmethod
341 +    def _switch_port_receive_bytes(statistic, resource_id, resource_meta):
342 +        return (int(statistic['in_bytes']), resource_id, resource_meta)
343 +
344 +    @staticmethod
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
348 new file mode 100644
349 index 0000000..993df58
350 --- /dev/null
351 +++ b/ceilometer/neutron_client.py
352 @@ -0,0 +1,73 @@
353 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
354 +#
355 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
356 +#
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
360 +#
361 +#      http://www.apache.org/licenses/LICENSE-2.0
362 +#
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.
368 +
369 +import functools
370 +
371 +from neutronclient.v2_0 import client as clientv20
372 +from oslo.config import cfg
373 +
374 +from ceilometer.openstack.common import log
375 +
376 +cfg.CONF.import_group('service_credentials', 'ceilometer.service')
377 +
378 +LOG = log.getLogger(__name__)
379 +
380 +
381 +def logged(func):
382 +
383 +    @functools.wraps(func)
384 +    def with_logging(*args, **kwargs):
385 +        try:
386 +            return func(*args, **kwargs)
387 +        except Exception as e:
388 +            LOG.exception(e)
389 +            raise
390 +
391 +    return with_logging
392 +
393 +
394 +class Client(object):
395 +    """A client which gets information via python-neutronclient."""
396 +
397 +    def __init__(self):
398 +        conf = cfg.CONF.service_credentials
399 +        params = {
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
407 +        }
408 +
409 +        if conf.os_tenant_id:
410 +            params['tenant_id'] = conf.os_tenant_id
411 +        else:
412 +            params['tenant_name'] = conf.os_tenant_name
413 +
414 +        self.client = clientv20.Client(**params)
415 +
416 +    @logged
417 +    def network_get_all(self):
418 +        """Returns all networks."""
419 +        resp = self.client.list_networks()
420 +        return resp.get('networks')
421 +
422 +    @logged
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
427 new file mode 100644
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
430 new file mode 100644
431 index 0000000..1817a32
432 --- /dev/null
433 +++ b/ceilometer/tests/network/statistics/opencontrail/test_client.py
434 @@ -0,0 +1,76 @@
435 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
436 +#
437 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
438 +#
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
442 +#
443 +#      http://www.apache.org/licenses/LICENSE-2.0
444 +#
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.
450 +
451 +import mock
452 +
453 +from ceilometer.network.statistics.opencontrail import client
454 +from ceilometer.openstack.common import test
455 +
456 +
457 +class TestOpencontrailClient(test.BaseTestCase):
458 +
459 +    def setUp(self):
460 +        super(TestOpencontrailClient, self).setUp()
461 +        self.client = client.Client('http://127.0.0.1:8143',
462 +                                    'admin', 'admin', None, False)
463 +
464 +        self.post_resp = mock.MagicMock()
465 +        self.post = mock.patch('requests.post',
466 +                               return_value=self.post_resp).start()
467 +
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'
474 +
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'
481 +
482 +    def test_port_statistics(self):
483 +        uuid = 'bbb'
484 +        self.client.networks.get_port_statistics(uuid)
485 +
486 +        call_args = self.post.call_args_list[0][0]
487 +        call_kwargs = self.post.call_args_list[0][1]
488 +
489 +        expected_url = 'http://127.0.0.1:8143/authenticate'
490 +        self.assertEqual(expected_url, call_args[0])
491 +
492 +        data = call_kwargs.get('data')
493 +        expected_data = {'domain': None, 'password': 'admin',
494 +                         'username': 'admin'}
495 +        self.assertEqual(expected_data, data)
496 +
497 +        call_args = self.get.call_args_list[0][0]
498 +        call_kwargs = self.get.call_args_list[0][1]
499 +
500 +        expected_url = ('http://127.0.0.1:8143/api/tenant/'
501 +                        'networking/virtual-machines/details')
502 +        self.assertEqual(expected_url, call_args[0])
503 +
504 +        data = call_kwargs.get('data')
505 +        cookies = call_kwargs.get('cookies')
506 +
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
512 new file mode 100644
513 index 0000000..940e998
514 --- /dev/null
515 +++ b/ceilometer/tests/network/statistics/opencontrail/test_driver.py
516 @@ -0,0 +1,145 @@
517 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
518 +#
519 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
520 +#
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
524 +#
525 +#      http://www.apache.org/licenses/LICENSE-2.0
526 +#
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.
532 +
533 +import mock
534 +from six.moves.urllib import parse as url_parse
535 +
536 +from ceilometer.network.statistics.opencontrail import driver
537 +from ceilometer.openstack.common import test
538 +
539 +
540 +class TestOpencontrailDriver(test.BaseTestCase):
541 +
542 +    def setUp(self):
543 +        super(TestOpencontrailDriver, self).setUp()
544 +
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()
549 +
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()
554 +
555 +        self.driver = driver.OpencontrailDriver()
556 +        self.parse_url = url_parse.ParseResult('opencontrail',
557 +                                               '127.0.0.1:8143',
558 +                                               '/', None, None, None)
559 +        self.params = {'password': ['admin'],
560 +                       'scheme': ['http'],
561 +                       'username': ['admin'],
562 +                       'verify_ssl': ['false']}
563 +
564 +    @staticmethod
565 +    def fake_ports():
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',
572 +                 'name': '',
573 +                 'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
574 +                 'status': 'ACTIVE',
575 +                 'tenant_id': '89271fa581ab4380bf172f868c3615f9'}]
576 +
577 +    @staticmethod
578 +    def fake_networks():
579 +        return [{'admin_state_up': True,
580 +                 'id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
581 +                 'name': 'public',
582 +                 'provider:network_type': 'gre',
583 +                 'provider:physical_network': None,
584 +                 'provider:segmentation_id': 2,
585 +                 'router:external': True,
586 +                 'shared': False,
587 +                 'status': 'ACTIVE',
588 +                 'subnets': [u'c4b6f5b8-3508-4896-b238-a441f25fb492'],
589 +                 'tenant_id': '62d6f08bbd3a44f6ad6f00ca15cce4e5'}]
590 +
591 +    @staticmethod
592 +    def fake_port_stats():
593 +        return {"value": [{
594 +            "name": "c588ebb7-ae52-485a-9f0c-b2791c5da196",
595 +            "value": {
596 +                "UveVirtualMachineAgent": {
597 +                    "if_stats_list": [{
598 +                        "out_bytes": 22,
599 +                        "in_bandwidth_usage": 0,
600 +                        "in_bytes": 23,
601 +                        "out_bandwidth_usage": 0,
602 +                        "out_pkts": 5,
603 +                        "in_pkts": 6,
604 +                        "name": ("674e553b-8df9-4321-87d9-93ba05b93558:"
605 +                                 "96d49cc3-4e01-40ce-9cac-c0e32642a442")
606 +                    }]}}}]}
607 +
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:
614 +
615 +            samples = self.driver.get_sample_data(meter_name, self.parse_url,
616 +                                                  self.params, {})
617 +
618 +            self.assertEqual(expected, [s for s in samples])
619 +
620 +            net_id = '298a3088-a446-4d5a-bad8-f92ecacd786b'
621 +            port_stats.assert_called_with(net_id)
622 +
623 +    def test_switch_port_receive_packets(self):
624 +        expected = [
625 +            (6,
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'},
630 +             mock.ANY)]
631 +        self._test_meter('switch.port.receive.packets', expected)
632 +
633 +    def test_switch_port_transmit_packets(self):
634 +        expected = [
635 +            (5,
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'},
640 +             mock.ANY)]
641 +        self._test_meter('switch.port.transmit.packets', expected)
642 +
643 +    def test_switch_port_receive_bytes(self):
644 +        expected = [
645 +            (23,
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'},
650 +             mock.ANY)]
651 +        self._test_meter('switch.port.receive.bytes', expected)
652 +
653 +    def test_switch_port_transmit_bytes(self):
654 +        expected = [
655 +            (22,
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'},
660 +             mock.ANY)]
661 +        self._test_meter('switch.port.transmit.bytes', expected)
662 diff --git a/ceilometer/tests/test_neutronclient.py b/ceilometer/tests/test_neutronclient.py
663 new file mode 100644
664 index 0000000..17d39b6
665 --- /dev/null
666 +++ b/ceilometer/tests/test_neutronclient.py
667 @@ -0,0 +1,74 @@
668 +# Copyright (C) 2014 eNovance SAS <licensing@enovance.com>
669 +#
670 +# Author: Sylvain Afchain <sylvain.afchain@enovance.com>
671 +#
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
675 +#
676 +#      http://www.apache.org/licenses/LICENSE-2.0
677 +#
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.
683 +
684 +from mock import patch
685 +
686 +from ceilometer import neutron_client
687 +from ceilometer.openstack.common import test
688 +
689 +
690 +class TestNeutronClient(test.BaseTestCase):
691 +
692 +    def setUp(self):
693 +        super(TestNeutronClient, self).setUp()
694 +        self.nc = neutron_client.Client()
695 +
696 +    @staticmethod
697 +    def fake_ports_list():
698 +        return {'ports':
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',
705 +                  'name': '',
706 +                  'network_id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
707 +                  'status': 'ACTIVE',
708 +                  'tenant_id': '89271fa581ab4380bf172f868c3615f9'}]}
709 +
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()
714 +
715 +        self.assertEqual(1, len(ports))
716 +        self.assertEqual('96d49cc3-4e01-40ce-9cac-c0e32642a442',
717 +                         ports[0]['id'])
718 +
719 +    @staticmethod
720 +    def fake_networks_list():
721 +        return {'networks':
722 +                [{'admin_state_up': True,
723 +                  'id': '298a3088-a446-4d5a-bad8-f92ecacd786b',
724 +                  'name': 'public',
725 +                  'provider:network_type': 'gre',
726 +                  'provider:physical_network': None,
727 +                  'provider:segmentation_id': 2,
728 +                  'router:external': True,
729 +                  'shared': False,
730 +                  'status': 'ACTIVE',
731 +                  'subnets': [u'c4b6f5b8-3508-4896-b238-a441f25fb492'],
732 +                  'tenant_id': '62d6f08bbd3a44f6ad6f00ca15cce4e5'}]}
733 +
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()
738 +
739 +        self.assertEqual(1, len(networks))
740 +        self.assertEqual('298a3088-a446-4d5a-bad8-f92ecacd786b',
741 +                         networks[0]['id'])
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
751  pytz>=2010h
752  PyYAML>=3.1.0
753  requests>=1.1
754 diff --git a/setup.cfg b/setup.cfg
755 index 8cd49ed..b7ce034 100644
756 --- a/setup.cfg
757 +++ b/setup.cfg
758 @@ -200,6 +200,7 @@ ceilometer.dispatcher =
759  
760  network.statistics.drivers =
761      opendaylight = ceilometer.network.statistics.opendaylight.driver:OpenDayLightDriver
762 +    opencontrail = ceilometer.network.statistics.opencontrail.driver:OpencontrailDriver
763  
764  
765  [build_sphinx]