summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgordon chung <gord@live.ca>2015-01-20 18:14:51 -0500
committergordon chung <gord@live.ca>2015-01-20 18:36:18 -0500
commit5a5dbbbee783e0dbfb089cb36523b1df41b76e72 (patch)
tree31ccbfef3128e4a651c2900bf1aaf7d6d3f2819e
parent2ca24af1fb92c85b6e7bf91a8a4cc5686d59bbc4 (diff)
downloadceilometermiddleware-5a5dbbbee783e0dbfb089cb36523b1df41b76e72.tar.gz
initial checkin
-rw-r--r--.coveragerc7
-rw-r--r--.gitignore3
-rw-r--r--.gitreview4
-rw-r--r--.testr.conf4
-rw-r--r--AUTHORS1
-rw-r--r--CONTRIBUTING.rst16
-rw-r--r--ChangeLog4
-rw-r--r--HACKING.rst4
-rw-r--r--LICENSE30
-rw-r--r--MANIFEST.in6
-rw-r--r--README.rst15
-rw-r--r--babel.cfg2
-rw-r--r--ceilometermiddleware.egg-info/PKG-INFO38
-rw-r--r--ceilometermiddleware.egg-info/SOURCES.txt35
-rw-r--r--ceilometermiddleware.egg-info/dependency_links.txt1
-rw-r--r--ceilometermiddleware.egg-info/not-zip-safe1
-rw-r--r--ceilometermiddleware.egg-info/pbr.json1
-rw-r--r--ceilometermiddleware.egg-info/requires.txt8
-rw-r--r--ceilometermiddleware.egg-info/top_level.txt1
-rw-r--r--ceilometermiddleware/__init__.py19
-rw-r--r--ceilometermiddleware/__init__.pycbin0 -> 276 bytes
-rw-r--r--ceilometermiddleware/swift.py225
-rw-r--r--ceilometermiddleware/swift.pycbin0 -> 7443 bytes
-rw-r--r--ceilometermiddleware/tests/__init__.py0
-rw-r--r--ceilometermiddleware/tests/__init__.pycbin0 -> 125 bytes
-rw-r--r--ceilometermiddleware/tests/base.py23
-rw-r--r--ceilometermiddleware/tests/base.pycbin0 -> 437 bytes
-rw-r--r--ceilometermiddleware/tests/test_swift.py330
-rw-r--r--ceilometermiddleware/tests/test_swift.pycbin0 -> 14967 bytes
-rwxr-xr-xdoc/source/conf.py75
-rw-r--r--doc/source/contributing.rst4
-rw-r--r--doc/source/index.rst25
-rw-r--r--doc/source/installation.rst12
-rw-r--r--doc/source/readme.rst1
-rw-r--r--doc/source/usage.rst7
-rw-r--r--openstack-common.conf6
-rw-r--r--requirements.txt12
-rw-r--r--setup.cfg47
-rwxr-xr-xsetup.py22
-rw-r--r--test-requirements.txt15
-rw-r--r--tox.ini37
41 files changed, 1012 insertions, 29 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..eebff58
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,7 @@
+[run]
+branch = True
+source = ceilometermiddleware
+omit = ceilometermiddleware/tests/*
+
+[report]
+ignore-errors = True
diff --git a/.gitignore b/.gitignore
index db4561e..1981734 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,7 @@ lib64/
parts/
sdist/
var/
-*.egg-info/
+*.egg-info
.installed.cfg
*.egg
@@ -35,6 +35,7 @@ pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
+.testrespository
.coverage
.cache
nosetests.xml
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000..cd1c5d3
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,4 @@
+[gerrit]
+host=review.openstack.org
+port=29418
+project=openstack/ceilometermiddleware.git
diff --git a/.testr.conf b/.testr.conf
new file mode 100644
index 0000000..c7e817f
--- /dev/null
+++ b/.testr.conf
@@ -0,0 +1,4 @@
+[DEFAULT]
+test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./ceilometermiddleware/tests $LISTOPT $IDOPTION
+test_id_option=--load-list $IDFILE
+test_list_option=--list
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..2cf6b2e
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+gordon chung <gord@live.ca>
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..72e28be
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,16 @@
+If you would like to contribute to the development of OpenStack,
+you must follow the steps in this page:
+
+ http://docs.openstack.org/infra/manual/developers.html
+
+Once those steps have been completed, changes to OpenStack
+should be submitted for review via the Gerrit tool, following
+the workflow documented at:
+
+ http://docs.openstack.org/infra/manual/developers.html#development-workflow
+
+Pull requests submitted through GitHub will be ignored.
+
+Bugs should be filed on Launchpad, not GitHub:
+
+ https://bugs.launchpad.net/ceilometermiddleware
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..efe1d67
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,4 @@
+CHANGES
+=======
+
+* Initial Cookiecutter Commit
diff --git a/HACKING.rst b/HACKING.rst
new file mode 100644
index 0000000..7f9f3a8
--- /dev/null
+++ b/HACKING.rst
@@ -0,0 +1,4 @@
+ceilometermiddleware Style Commandments
+===============================================
+
+Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
diff --git a/LICENSE b/LICENSE
index e06d208..68c771a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,5 @@
-Apache License
+
+ Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -173,30 +174,3 @@ Apache License
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright {yyyy} {name of copyright owner}
-
- 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.
-
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..c978a52
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include AUTHORS
+include ChangeLog
+exclude .gitignore
+exclude .gitreview
+
+global-exclude *.pyc
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..dd27982
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,15 @@
+===============================
+ceilometermiddleware
+===============================
+
+OpenStack Telemetry middleware for generating metrics
+
+* Free software: Apache license
+* Documentation: http://docs.openstack.org/developer/ceilometermiddleware
+* Source: http://git.openstack.org/cgit/openstack/ceilometermiddleware
+* Bugs: http://bugs.launchpad.net/ceilometer
+
+Features
+--------
+
+* TODO
diff --git a/babel.cfg b/babel.cfg
new file mode 100644
index 0000000..15cd6cb
--- /dev/null
+++ b/babel.cfg
@@ -0,0 +1,2 @@
+[python: **.py]
+
diff --git a/ceilometermiddleware.egg-info/PKG-INFO b/ceilometermiddleware.egg-info/PKG-INFO
new file mode 100644
index 0000000..22e18b0
--- /dev/null
+++ b/ceilometermiddleware.egg-info/PKG-INFO
@@ -0,0 +1,38 @@
+Metadata-Version: 1.1
+Name: ceilometermiddleware
+Version: 0.0.0.post1
+Summary: OpenStack Telemetry middleware for generating metrics
+Home-page: http://www.openstack.org/
+Author: OpenStack
+Author-email: openstack-dev@lists.openstack.org
+License: UNKNOWN
+Description: ===============================
+ ceilometermiddleware
+ ===============================
+
+ OpenStack Telemetry middleware for generating metrics
+
+ * Free software: Apache license
+ * Documentation: http://docs.openstack.org/developer/ceilometermiddleware
+ * Source: http://git.openstack.org/cgit/openstack/ceilometermiddleware
+ * Bugs: http://bugs.launchpad.net/ceilometer
+
+ Features
+ --------
+
+ * TODO
+
+
+Platform: UNKNOWN
+Classifier: Environment :: OpenStack
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
diff --git a/ceilometermiddleware.egg-info/SOURCES.txt b/ceilometermiddleware.egg-info/SOURCES.txt
new file mode 100644
index 0000000..3f1d14b
--- /dev/null
+++ b/ceilometermiddleware.egg-info/SOURCES.txt
@@ -0,0 +1,35 @@
+.coveragerc
+.mailmap
+.testr.conf
+AUTHORS
+CONTRIBUTING.rst
+ChangeLog
+HACKING.rst
+LICENSE
+MANIFEST.in
+README.rst
+babel.cfg
+openstack-common.conf
+requirements.txt
+setup.cfg
+setup.py
+test-requirements.txt
+tox.ini
+ceilometermiddleware/__init__.py
+ceilometermiddleware/swift.py
+ceilometermiddleware.egg-info/PKG-INFO
+ceilometermiddleware.egg-info/SOURCES.txt
+ceilometermiddleware.egg-info/dependency_links.txt
+ceilometermiddleware.egg-info/not-zip-safe
+ceilometermiddleware.egg-info/pbr.json
+ceilometermiddleware.egg-info/requires.txt
+ceilometermiddleware.egg-info/top_level.txt
+ceilometermiddleware/tests/__init__.py
+ceilometermiddleware/tests/base.py
+ceilometermiddleware/tests/test_swift.py
+doc/source/conf.py
+doc/source/contributing.rst
+doc/source/index.rst
+doc/source/installation.rst
+doc/source/readme.rst
+doc/source/usage.rst \ No newline at end of file
diff --git a/ceilometermiddleware.egg-info/dependency_links.txt b/ceilometermiddleware.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/ceilometermiddleware.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/ceilometermiddleware.egg-info/not-zip-safe b/ceilometermiddleware.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/ceilometermiddleware.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/ceilometermiddleware.egg-info/pbr.json b/ceilometermiddleware.egg-info/pbr.json
new file mode 100644
index 0000000..e36cd2e
--- /dev/null
+++ b/ceilometermiddleware.egg-info/pbr.json
@@ -0,0 +1 @@
+{"is_release": false, "git_version": "e8cd146"} \ No newline at end of file
diff --git a/ceilometermiddleware.egg-info/requires.txt b/ceilometermiddleware.egg-info/requires.txt
new file mode 100644
index 0000000..c25640b
--- /dev/null
+++ b/ceilometermiddleware.egg-info/requires.txt
@@ -0,0 +1,8 @@
+oslo.config>=1.6.0
+oslo.context>=0.1.0
+oslo.messaging>=1.4.0,!=1.5.0
+oslo.utils>=1.2.0
+pbr>=0.6,!=0.7,<1.0
+pycadf>=0.6.0
+six>=1.7.0
+Babel>=1.3
diff --git a/ceilometermiddleware.egg-info/top_level.txt b/ceilometermiddleware.egg-info/top_level.txt
new file mode 100644
index 0000000..afdc788
--- /dev/null
+++ b/ceilometermiddleware.egg-info/top_level.txt
@@ -0,0 +1 @@
+ceilometermiddleware
diff --git a/ceilometermiddleware/__init__.py b/ceilometermiddleware/__init__.py
new file mode 100644
index 0000000..f5cdc8c
--- /dev/null
+++ b/ceilometermiddleware/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+# 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 pbr.version
+
+
+__version__ = pbr.version.VersionInfo(
+ 'ceilometermiddleware').version_string()
diff --git a/ceilometermiddleware/__init__.pyc b/ceilometermiddleware/__init__.pyc
new file mode 100644
index 0000000..1c4892f
--- /dev/null
+++ b/ceilometermiddleware/__init__.pyc
Binary files differ
diff --git a/ceilometermiddleware/swift.py b/ceilometermiddleware/swift.py
new file mode 100644
index 0000000..8f27a1c
--- /dev/null
+++ b/ceilometermiddleware/swift.py
@@ -0,0 +1,225 @@
+#
+# Copyright 2012 eNovance <licensing@enovance.com>
+#
+# 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.
+
+"""
+Telemetry Middleware for Swift Proxy
+
+Configuration:
+In /etc/swift/proxy-server.conf on the main pipeline add "ceilometer" just
+before "proxy-server" and add the following filter in the file:
+.. code-block:: python
+ [filter:ceilometer]
+ paste.filter_factory = ceilometermiddleware.swift:R
+ # Some optional configuration this allow to publish additional metadata
+ metadata_headers = X-TEST
+ # Set reseller prefix (defaults to "AUTH_" if not set)
+ reseller_prefix = AUTH_
+"""
+import functools
+import logging
+
+import oslo.messaging
+from oslo_config import cfg
+from oslo_context import context
+from oslo_utils import timeutils
+from pycadf import event as cadf_event
+from pycadf import measurement as cadf_measurement
+from pycadf import metric as cadf_metric
+from pycadf import resource as cadf_resource
+import six
+import six.moves.urllib.parse as urlparse
+
+_LOG = logging.getLogger(__name__)
+
+
+def _log_and_ignore_error(fn):
+ @functools.wraps(fn)
+ def wrapper(*args, **kwargs):
+ try:
+ return fn(*args, **kwargs)
+ except Exception as e:
+ _LOG.exception('An exception occurred processing '
+ 'the API call: %s ', e)
+ return wrapper
+
+
+class InputProxy(object):
+ """File-like object that counts bytes read.
+
+ To be swapped in for wsgi.input for accounting purposes.
+ Borrowed from swift.common.utils. Duplicated here to avoid
+ dependency on swift package.
+ """
+ def __init__(self, wsgi_input):
+ self.wsgi_input = wsgi_input
+ self.bytes_received = 0
+
+ def read(self, *args, **kwargs):
+ """Pass read request to the underlying file-like object
+
+ Add bytes read to total.
+ """
+ chunk = self.wsgi_input.read(*args, **kwargs)
+ self.bytes_received += len(chunk)
+ return chunk
+
+ def readline(self, *args, **kwargs):
+ """Pass readline request to the underlying file-like object
+
+ Add bytes read to total.
+ """
+ line = self.wsgi_input.readline(*args, **kwargs)
+ self.bytes_received += len(line)
+ return line
+
+
+class Swift(object):
+ """Swift middleware used for counting requests."""
+
+ def __init__(self, app, conf):
+ self._app = app
+
+ self._notifier = oslo.messaging.Notifier(
+ oslo.messaging.get_transport(cfg.CONF),
+ publisher_id='ceilometermiddleware')
+
+ self.metadata_headers = [h.strip().replace('-', '_').lower()
+ for h in conf.get(
+ "metadata_headers",
+ "").split(",") if h.strip()]
+
+ self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_')
+ if self.reseller_prefix and self.reseller_prefix[-1] != '_':
+ self.reseller_prefix += '_'
+
+ def __call__(self, env, start_response):
+ start_response_args = [None]
+ input_proxy = InputProxy(env['wsgi.input'])
+ env['wsgi.input'] = input_proxy
+
+ def my_start_response(status, headers, exc_info=None):
+ start_response_args[0] = (status, list(headers), exc_info)
+
+ def iter_response(iterable):
+ iterator = iter(iterable)
+ try:
+ chunk = next(iterator)
+ while not chunk:
+ chunk = next(iterator)
+ except StopIteration:
+ chunk = ''
+
+ if start_response_args[0]:
+ start_response(*start_response_args[0])
+ bytes_sent = 0
+ try:
+ while chunk:
+ bytes_sent += len(chunk)
+ yield chunk
+ chunk = next(iterator)
+ finally:
+ self.emit_event(env, input_proxy.bytes_received, bytes_sent)
+
+ try:
+ iterable = self._app(env, my_start_response)
+ except Exception:
+ self.emit_event(env, input_proxy.bytes_received, 0, 'failure')
+ raise
+ else:
+ return iter_response(iterable)
+
+ @_log_and_ignore_error
+ def emit_event(self, env, bytes_received, bytes_sent, outcome='success'):
+ path = urlparse.quote(env['PATH_INFO'])
+ method = env['REQUEST_METHOD']
+ headers = {}
+ for header in env:
+ if header.startswith('HTTP_') and env[header]:
+ key = header[5:]
+ if isinstance(env[header], six.text_type):
+ headers[key] = env[header].encode('utf-8')
+ else:
+ headers[key] = str(env[header])
+
+ try:
+ container = obj = None
+ version, account, remainder = path.replace(
+ '/', '', 1).split('/', 2)
+ if not version or not account:
+ raise ValueError('Invalid path: %s' % path)
+ if remainder:
+ if '/' in remainder:
+ container, obj = remainder.split('/', 1)
+ else:
+ container = remainder
+ except ValueError:
+ return
+
+ now = timeutils.utcnow().isoformat()
+
+ resource_metadata = {
+ "path": path,
+ "version": version,
+ "container": container,
+ "object": obj,
+ }
+
+ for header in self.metadata_headers:
+ if header.upper() in headers:
+ resource_metadata['http_header_%s' % header] = headers.get(
+ header.upper())
+
+ # build object store details
+ target = cadf_resource.Resource(
+ typeURI='service/storage/object',
+ id=account.partition(self.reseller_prefix)[2])
+ target.metadata = resource_metadata
+ target.action = method.lower()
+
+ # build user details
+ initiator = cadf_resource.Resource(
+ typeURI='service/security/account/user',
+ id=env.get('HTTP_X_USER_ID'))
+ initiator.project_id = env.get('HTTP_X_TENANT_ID')
+
+ # build notification body
+ event = cadf_event.Event(eventTime=now, outcome=outcome,
+ initiator=initiator, target=target,
+ observer=cadf_resource.Resource(id='target'))
+
+ # measurements
+ if bytes_received:
+ event.add_measurement(cadf_measurement.Measurement(
+ result=bytes_received,
+ metric=cadf_metric.Metric(
+ name='storage.objects.incoming.bytes', unit='B')))
+ if bytes_sent:
+ event.add_measurement(cadf_measurement.Measurement(
+ result=bytes_sent,
+ metric=cadf_metric.Metric(
+ name='storage.objects.outgoing.bytes', unit='B')))
+
+ self._notifier.info(context.get_admin_context().to_dict(),
+ 'objectstore.http.request', event.as_dict())
+
+
+def filter_factory(global_conf, **local_conf):
+ conf = global_conf.copy()
+ conf.update(local_conf)
+
+ def filter(app):
+ return Swift(app, conf)
+ return filter
diff --git a/ceilometermiddleware/swift.pyc b/ceilometermiddleware/swift.pyc
new file mode 100644
index 0000000..c8ce92e
--- /dev/null
+++ b/ceilometermiddleware/swift.pyc
Binary files differ
diff --git a/ceilometermiddleware/tests/__init__.py b/ceilometermiddleware/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ceilometermiddleware/tests/__init__.py
diff --git a/ceilometermiddleware/tests/__init__.pyc b/ceilometermiddleware/tests/__init__.pyc
new file mode 100644
index 0000000..3ce97fd
--- /dev/null
+++ b/ceilometermiddleware/tests/__init__.pyc
Binary files differ
diff --git a/ceilometermiddleware/tests/base.py b/ceilometermiddleware/tests/base.py
new file mode 100644
index 0000000..1c30cdb
--- /dev/null
+++ b/ceilometermiddleware/tests/base.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2010-2011 OpenStack Foundation
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+from oslotest import base
+
+
+class TestCase(base.BaseTestCase):
+
+ """Test case base class for all unit tests."""
diff --git a/ceilometermiddleware/tests/base.pyc b/ceilometermiddleware/tests/base.pyc
new file mode 100644
index 0000000..28ddfc3
--- /dev/null
+++ b/ceilometermiddleware/tests/base.pyc
Binary files differ
diff --git a/ceilometermiddleware/tests/test_swift.py b/ceilometermiddleware/tests/test_swift.py
new file mode 100644
index 0000000..adfd167
--- /dev/null
+++ b/ceilometermiddleware/tests/test_swift.py
@@ -0,0 +1,330 @@
+#
+# Copyright 2012 eNovance <licensing@enovance.com>
+#
+# 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 cfg
+from oslo_utils import timeutils
+import six
+
+from ceilometermiddleware import swift
+from ceilometermiddleware.tests import base as tests_base
+
+
+class FakeApp(object):
+ def __init__(self, body=None):
+ self.body = body or ['This string is 28 bytes long']
+
+ def __call__(self, env, start_response):
+ yield
+ start_response('200 OK', [
+ ('Content-Type', 'text/plain'),
+ ('Content-Length', str(sum(map(len, self.body))))
+ ])
+ while env['wsgi.input'].read(5):
+ pass
+ for line in self.body:
+ yield line
+
+
+class FakeRequest(object):
+ """A bare bones request object
+
+ The middleware will inspect this for request method,
+ wsgi.input and headers.
+ """
+
+ def __init__(self, path, environ=None, headers=None):
+ environ = environ or {}
+ headers = headers or {}
+
+ environ['PATH_INFO'] = path
+
+ if 'wsgi.input' not in environ:
+ environ['wsgi.input'] = six.moves.cStringIO('')
+
+ for header, value in headers.iteritems():
+ environ['HTTP_%s' % header.upper()] = value
+ self.environ = environ
+
+
+@mock.patch('oslo.messaging.get_transport', mock.MagicMock())
+class TestSwift(tests_base.TestCase):
+
+ def setUp(self):
+ super(TestSwift, self).setUp()
+ cfg.CONF([], project='ceilometermiddleware')
+ self.addCleanup(cfg.CONF.reset)
+
+ @staticmethod
+ def start_response(*args):
+ pass
+
+ def test_get(self):
+ app = swift.Swift(FakeApp(), {})
+ req = FakeRequest('/1.0/account/container/obj',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ resp = app(req.environ, self.start_response)
+ self.assertEqual(["This string is 28 bytes long"], list(resp))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ self.assertEqual(28, data[2]['measurements'][0]['result'])
+ self.assertEqual('storage.objects.outgoing.bytes',
+ data[2]['measurements'][0]['metric']['name'])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertEqual('obj', metadata['object'])
+ self.assertEqual('get', data[2]['target']['action'])
+
+ def test_put(self):
+ app = swift.Swift(FakeApp(body=['']), {})
+ req = FakeRequest(
+ '/1.0/account/container/obj',
+ environ={'REQUEST_METHOD': 'PUT',
+ 'wsgi.input':
+ six.moves.cStringIO('some stuff')})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ self.assertEqual(10, data[2]['measurements'][0]['result'])
+ self.assertEqual('storage.objects.incoming.bytes',
+ data[2]['measurements'][0]['metric']['name'])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertEqual('obj', metadata['object'])
+ self.assertEqual('put', data[2]['target']['action'])
+
+ def test_post(self):
+ app = swift.Swift(FakeApp(body=['']), {})
+ req = FakeRequest(
+ '/1.0/account/container/obj',
+ environ={'REQUEST_METHOD': 'POST',
+ 'wsgi.input': six.moves.cStringIO('some other stuff')})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ self.assertEqual(16, data[2]['measurements'][0]['result'])
+ self.assertEqual('storage.objects.incoming.bytes',
+ data[2]['measurements'][0]['metric']['name'])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertEqual('obj', metadata['object'])
+ self.assertEqual('post', data[2]['target']['action'])
+
+ def test_head(self):
+ app = swift.Swift(FakeApp(body=['']), {})
+ req = FakeRequest('/1.0/account/container/obj',
+ environ={'REQUEST_METHOD': 'HEAD'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ self.assertIsNone(data[2].get('measurements'))
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertEqual('obj', metadata['object'])
+ self.assertEqual('head', data[2]['target']['action'])
+
+ def test_bogus_request(self):
+ """Test even for arbitrary request method, this will still work."""
+ app = swift.Swift(FakeApp(body=['']), {})
+ req = FakeRequest('/1.0/account/container/obj',
+ environ={'REQUEST_METHOD': 'BOGUS'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ self.assertIsNone(data[2].get('measurements'))
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertEqual('obj', metadata['object'])
+ self.assertEqual('bogus', data[2]['target']['action'])
+
+ def test_get_container(self):
+ app = swift.Swift(FakeApp(), {})
+ req = FakeRequest('/1.0/account/container',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ self.assertEqual(28, data[2]['measurements'][0]['result'])
+ self.assertEqual('storage.objects.outgoing.bytes',
+ data[2]['measurements'][0]['metric']['name'])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertIsNone(metadata['object'])
+ self.assertEqual('get', data[2]['target']['action'])
+
+ def test_no_metadata_headers(self):
+ app = swift.Swift(FakeApp(), {})
+ req = FakeRequest('/1.0/account/container',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertIsNone(metadata['object'])
+ self.assertEqual('get', data[2]['target']['action'])
+ http_headers = [k for k in metadata.keys()
+ if k.startswith('http_header_')]
+ self.assertEqual(0, len(http_headers))
+
+ def test_metadata_headers(self):
+ app = swift.Swift(FakeApp(), {
+ 'metadata_headers': 'X_VAR1, x-var2, x-var3, token'
+ })
+ req = FakeRequest('/1.0/account/container',
+ environ={'REQUEST_METHOD': 'GET'},
+ headers={'X_VAR1': 'value1',
+ 'X_VAR2': 'value2',
+ 'TOKEN': 'token'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertIsNone(metadata['object'])
+ self.assertEqual('get', data[2]['target']['action'])
+ http_headers = [k for k in metadata.keys()
+ if k.startswith('http_header_')]
+ self.assertEqual(3, len(http_headers))
+ self.assertEqual('value1', metadata['http_header_x_var1'])
+ self.assertEqual('value2', metadata['http_header_x_var2'])
+ self.assertEqual('token', metadata['http_header_token'])
+ self.assertFalse('http_header_x_var3' in metadata)
+
+ def test_metadata_headers_unicode(self):
+ app = swift.Swift(FakeApp(), {
+ 'metadata_headers': 'unicode'
+ })
+ uni = u'\xef\xbd\xa1\xef\xbd\xa5'
+ req = FakeRequest('/1.0/account/container',
+ environ={'REQUEST_METHOD': 'GET'},
+ headers={'UNICODE': uni})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertIsNone(metadata['object'])
+ self.assertEqual('get', data[2]['target']['action'])
+ http_headers = [k for k in metadata.keys()
+ if k.startswith('http_header_')]
+ self.assertEqual(1, len(http_headers))
+ self.assertEqual(uni.encode('utf-8'),
+ metadata['http_header_unicode'])
+
+ def test_metadata_headers_on_not_existing_header(self):
+ app = swift.Swift(FakeApp(), {
+ 'metadata_headers': 'x-var3'
+ })
+ req = FakeRequest('/1.0/account/container',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual('objectstore.http.request', data[1])
+ metadata = data[2]['target']['metadata']
+ self.assertEqual('1.0', metadata['version'])
+ self.assertEqual('container', metadata['container'])
+ self.assertIsNone(metadata['object'])
+ self.assertEqual('get', data[2]['target']['action'])
+ http_headers = [k for k in metadata.keys()
+ if k.startswith('http_header_')]
+ self.assertEqual(0, len(http_headers))
+
+ def test_bogus_path(self):
+ app = swift.Swift(FakeApp(), {})
+ req = FakeRequest('/5.0//',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(0, len(notify.call_args_list))
+
+ def test_missing_resource_id(self):
+ app = swift.Swift(FakeApp(), {})
+ req = FakeRequest('/v1/', environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(0, len(notify.call_args_list))
+
+ @mock.patch.object(timeutils, 'utcnow')
+ def test_emit_event_fail(self, mocked_time):
+ mocked_time.side_effect = Exception("a exception")
+ app = swift.Swift(FakeApp(body=["test"]), {})
+ req = FakeRequest('/1.0/account/container',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ resp = list(app(req.environ, self.start_response))
+ self.assertEqual(0, len(notify.call_args_list))
+ self.assertEqual(["test"], resp)
+
+ def test_reseller_prefix(self):
+ app = swift.Swift(FakeApp(), {})
+ req = FakeRequest('/1.0/AUTH_account/container/obj',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual("account", data[2]['target']['id'])
+
+ def test_custom_prefix(self):
+ app = swift.Swift(FakeApp(), {'reseller_prefix': 'CUSTOM_'})
+ req = FakeRequest('/1.0/CUSTOM_account/container/obj',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual("account", data[2]['target']['id'])
+
+ def test_invalid_reseller_prefix(self):
+ # Custom reseller prefix set, but without trailing underscore
+ app = swift.Swift(
+ FakeApp(), {'reseller_prefix': 'CUSTOM'})
+ req = FakeRequest('/1.0/CUSTOM_account/container/obj',
+ environ={'REQUEST_METHOD': 'GET'})
+ with mock.patch('oslo.messaging.Notifier.info') as notify:
+ list(app(req.environ, self.start_response))
+ self.assertEqual(1, len(notify.call_args_list))
+ data = notify.call_args_list[0][0]
+ self.assertEqual("account", data[2]['target']['id'])
diff --git a/ceilometermiddleware/tests/test_swift.pyc b/ceilometermiddleware/tests/test_swift.pyc
new file mode 100644
index 0000000..101abb6
--- /dev/null
+++ b/ceilometermiddleware/tests/test_swift.pyc
Binary files differ
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100755
index 0000000..989b422
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# 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 os
+import sys
+
+sys.path.insert(0, os.path.abspath('../..'))
+# -- General configuration ----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ #'sphinx.ext.intersphinx',
+ 'oslosphinx'
+]
+
+# autodoc generation is a bit aggressive and a nuisance when doing heavy
+# text edit cycles.
+# execute "export SPHINX_DEBUG=1" in your terminal to disable
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'ceilometermiddleware'
+copyright = u'2013, OpenStack Foundation'
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# -- Options for HTML output --------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+# html_theme_path = ["."]
+# html_theme = '_theme'
+# html_static_path = ['static']
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = '%sdoc' % project
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass
+# [howto/manual]).
+latex_documents = [
+ ('index',
+ '%s.tex' % project,
+ u'%s Documentation' % project,
+ u'OpenStack Foundation', 'manual'),
+]
+
+# Example configuration for intersphinx: refer to the Python standard library.
+#intersphinx_mapping = {'http://docs.python.org/': None}
diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst
new file mode 100644
index 0000000..1728a61
--- /dev/null
+++ b/doc/source/contributing.rst
@@ -0,0 +1,4 @@
+============
+Contributing
+============
+.. include:: ../../CONTRIBUTING.rst
diff --git a/doc/source/index.rst b/doc/source/index.rst
new file mode 100644
index 0000000..5db4146
--- /dev/null
+++ b/doc/source/index.rst
@@ -0,0 +1,25 @@
+.. ceilometermiddleware documentation master file, created by
+ sphinx-quickstart on Tue Jul 9 22:26:36 2013.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Welcome to ceilometermiddleware's documentation!
+========================================================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ readme
+ installation
+ usage
+ contributing
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/doc/source/installation.rst b/doc/source/installation.rst
new file mode 100644
index 0000000..6bd04aa
--- /dev/null
+++ b/doc/source/installation.rst
@@ -0,0 +1,12 @@
+============
+Installation
+============
+
+At the command line::
+
+ $ pip install ceilometermiddleware
+
+Or, if you have virtualenvwrapper installed::
+
+ $ mkvirtualenv ceilometermiddleware
+ $ pip install ceilometermiddleware
diff --git a/doc/source/readme.rst b/doc/source/readme.rst
new file mode 100644
index 0000000..a6210d3
--- /dev/null
+++ b/doc/source/readme.rst
@@ -0,0 +1 @@
+.. include:: ../../README.rst
diff --git a/doc/source/usage.rst b/doc/source/usage.rst
new file mode 100644
index 0000000..3210cbd
--- /dev/null
+++ b/doc/source/usage.rst
@@ -0,0 +1,7 @@
+========
+Usage
+========
+
+To use ceilometermiddleware in a project::
+
+ import ceilometermiddleware
diff --git a/openstack-common.conf b/openstack-common.conf
new file mode 100644
index 0000000..f8e454b
--- /dev/null
+++ b/openstack-common.conf
@@ -0,0 +1,6 @@
+[DEFAULT]
+
+# The list of modules to copy from oslo-incubator.git
+
+# The base module to hold the copy of openstack.common
+base=ceilometermiddleware
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..bfdc603
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,12 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+oslo.config>=1.6.0
+oslo.context>=0.1.0
+oslo.messaging>=1.4.0,!=1.5.0
+oslo.utils>=1.2.0
+pbr>=0.6,!=0.7,<1.0
+pycadf>=0.6.0
+six>=1.7.0
+Babel>=1.3
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..abcf0dc
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,47 @@
+[metadata]
+name = ceilometermiddleware
+summary = OpenStack Telemetry middleware for generating metrics
+description-file =
+ README.rst
+author = OpenStack
+author-email = openstack-dev@lists.openstack.org
+home-page = http://www.openstack.org/
+classifier =
+ Environment :: OpenStack
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+ License :: OSI Approved :: Apache Software License
+ Operating System :: POSIX :: Linux
+ Programming Language :: Python
+ Programming Language :: Python :: 2
+ Programming Language :: Python :: 2.7
+ Programming Language :: Python :: 2.6
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.3
+ Programming Language :: Python :: 3.4
+
+[files]
+packages =
+ ceilometermiddleware
+
+[build_sphinx]
+source-dir = doc/source
+build-dir = doc/build
+all_files = 1
+
+[upload_sphinx]
+upload-dir = doc/build/html
+
+[compile_catalog]
+directory = ceilometermiddleware/locale
+domain = ceilometermiddleware
+
+[update_catalog]
+domain = ceilometermiddleware
+output_dir = ceilometermiddleware/locale
+input_file = ceilometermiddleware/locale/ceilometermiddleware.pot
+
+[extract_messages]
+keywords = _ gettext ngettext l_ lazy_gettext
+mapping_file = babel.cfg
+output_file = ceilometermiddleware/locale/ceilometermiddleware.pot
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..70c2b3f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# 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.
+
+# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
+import setuptools
+
+setuptools.setup(
+ setup_requires=['pbr'],
+ pbr=True)
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..1803eb5
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,15 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+
+hacking>=0.10.0,<0.11
+
+coverage>=3.6
+discover
+python-subunit
+sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
+oslosphinx>=2.2.0
+oslotest>=1.2.0
+testrepository>=0.0.18
+testscenarios>=0.4
+testtools>=0.9.36,!=1.2.0
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..7363931
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,37 @@
+[tox]
+minversion = 1.6
+envlist = py33,py34,py26,py27,pypy,pep8
+skipsdist = True
+
+[testenv]
+usedevelop = True
+install_command = pip install -U {opts} {packages}
+setenv =
+ VIRTUAL_ENV={envdir}
+deps = -r{toxinidir}/requirements.txt
+ -r{toxinidir}/test-requirements.txt
+commands = python setup.py testr --slowest --testr-args='{posargs}'
+
+[testenv:pep8]
+commands = flake8
+
+[testenv:venv]
+commands = {posargs}
+
+[testenv:cover]
+commands = python setup.py testr --coverage --testr-args='{posargs}'
+
+[testenv:docs]
+commands = python setup.py build_sphinx
+
+[testenv:debug]
+commands = oslo_debug_helper {posargs}
+
+[flake8]
+# H803 skipped on purpose per list discussion.
+# E123, E125 skipped as they are invalid PEP-8.
+
+show-source = True
+ignore = E123,E125,H803
+builtins = _
+exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build