diff options
30 files changed, 831 insertions, 255 deletions
@@ -50,3 +50,6 @@ ChangeLog # Editors *~ .*.swp + +# reno build +releasenotes/build diff --git a/doc/source/cors.rst b/doc/source/cors.rst index dbd93a3..ea19d9e 100644 --- a/doc/source/cors.rst +++ b/doc/source/cors.rst @@ -45,39 +45,15 @@ domain, using oslo_config:: app = cors.CORS(your_wsgi_application, cfg.CONF) -In your application's config file, then include a default configuration block +In your application's config file, then include a configuration block something like this:: [cors] allowed_origin=https://website.example.com:443,https://website2.example.com:443 max_age=3600 allow_methods=GET,POST,PUT,DELETE - allow_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header - expose_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header - -This middleware permits you to override the rules for multiple -`allowed_origin`'s. To express this in your configuration file, first begin -with a `[cors]` group as above, into which you place your default -configuration values. Then add as many additional configuration groups as -necessary, naming them `[cors.something]` (each name must be unique). The -purpose of the suffix to `cors.` is legibility, we recommend using a -reasonable human-readable string:: - - [cors.ironic_webclient] - # CORS Configuration for a hypothetical ironic webclient, which overrides - # authentication - allowed_origin=https://ironic.example.com:443 - allow_credentials=True - - [cors.horizon] - # CORS Configuration for horizon, which uses global options. - allowed_origin=https://horizon.example.com:443 - - [cors.wildcard] - # CORS Configuration for the CORS specified domain wildcard, which only - # permits HTTP GET requests. - allowed_origin=* - allow_methods=GET + allow_headers=X-Custom-Header + expose_headers=X-Custom-Header Configuration for pastedeploy ----------------------------- @@ -90,8 +66,8 @@ will add CORS support.:: allowed_origin=https://website.example.com:443,https://website2.example.com:443 max_age=3600 allow_methods=GET,POST,PUT,DELETE - allow_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header - expose_headers=Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma,X-Custom-Header + allow_headers=X-Custom-Header + expose_headers=X-Custom-Header If your application is using pastedeploy, but would also like to use the existing configuration from oslo_config in order to simplify the points of diff --git a/oslo_middleware/base.py b/oslo_middleware/base.py index a98a058..27689a1 100644 --- a/oslo_middleware/base.py +++ b/oslo_middleware/base.py @@ -17,10 +17,22 @@ from inspect import getargspec import webob.dec +import webob.request +import webob.response from oslo_config import cfg +class NoContentTypeResponse(webob.response.Response): + + default_content_type = None # prevents webob assigning content type + + +class NoContentTypeRequest(webob.request.Request): + + ResponseClass = NoContentTypeResponse + + class ConfigurableMiddleware(object): """Base WSGI middleware wrapper. @@ -106,7 +118,7 @@ class ConfigurableMiddleware(object): """Do whatever you'd like to the response.""" return response - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=NoContentTypeRequest) def __call__(self, req): response = self.process_request(req) if response: diff --git a/oslo_middleware/cors.py b/oslo_middleware/cors.py index ffd54ed..b53ad45 100644 --- a/oslo_middleware/cors.py +++ b/oslo_middleware/cors.py @@ -12,17 +12,16 @@ # implied. See the License for the specific language governing permissions and # limitations under the License. -# Default allowed headers import copy from debtcollector import moves import logging +import debtcollector from oslo_config import cfg from oslo_middleware import base import six -import webob.dec import webob.exc -import webob.response + LOG = logging.getLogger(__name__) @@ -100,11 +99,6 @@ class InvalidOriginError(Exception): 'CORS request from origin \'%s\' not permitted.' % origin) -class _NoContentTypeResponse(webob.response.Response): - - default_content_type = None # prevents webob assigning content type - - class CORS(base.ConfigurableMiddleware): """CORS Middleware. @@ -205,6 +199,11 @@ class CORS(base.ConfigurableMiddleware): # prefixed with 'cors.' for section in self.oslo_conf.list_all_sections(): if section.startswith('cors.'): + debtcollector.deprecate('Multiple configuration blocks are ' + 'deprecated and will be removed in ' + 'future versions. Please consolidate ' + 'your configuration in the [cors] ' + 'configuration block.') # Register with the preconstructed defaults self.oslo_conf.register_opts(subgroup_opts, section) self.add_origin(**self.oslo_conf[section]) @@ -226,6 +225,9 @@ class CORS(base.ConfigurableMiddleware): # NOTE(dims): Support older code that still passes in # a string for allowed_origin instead of a list if isinstance(allowed_origin, six.string_types): + # TODO(krotscheck): https://review.openstack.org/#/c/312687/ + LOG.warning('DEPRECATED: The `allowed_origin` keyword argument in ' + '`add_origin()` should be a list, found String.') allowed_origin = [allowed_origin] if allowed_origin: @@ -330,7 +332,7 @@ class CORS(base.ConfigurableMiddleware): # underlying middleware's response content needs to be persisted. # Otherwise, create a new response. if 200 > response.status_code or response.status_code >= 300: - response = _NoContentTypeResponse(status=webob.exc.HTTPOk.code) + response = base.NoContentTypeResponse(status=webob.exc.HTTPOk.code) # Does the request have an origin header? (Section 6.2.1) if 'Origin' not in request.headers: diff --git a/oslo_middleware/healthcheck/__init__.py b/oslo_middleware/healthcheck/__init__.py index a821233..ce2b03a 100644 --- a/oslo_middleware/healthcheck/__init__.py +++ b/oslo_middleware/healthcheck/__init__.py @@ -52,32 +52,187 @@ def _expand_template(contents, params): class Healthcheck(base.ConfigurableMiddleware): """Healthcheck middleware used for monitoring. - If the path is /healthcheck, it will respond 200 with "OK" as the body. - Or 503 with the reason as the body if one of the backend report - an application issue. + If the path is ``/healthcheck``, it will respond 200 with "OK" as + the body. Or a 503 with the reason as the body if one of the backends + reports an application issue. This is useful for the following reasons: - 1. Load balancers can 'ping' this url to determine service availability. - 2. Provides an endpoint that is similar to 'mod_status' in apache which - can provide details (or no details, depending on if configured) about - the activity of the server. - - Example requests/responses: - - $ curl -i -X HEAD "http://0.0.0.0:8775/status" - HTTP/1.1 204 No Content - Content-Type: text/plain; charset=UTF-8 - Content-Length: 0 - Date: Fri, 11 Sep 2015 18:55:08 GMT - - $ curl -i "http://0.0.0.0:8775/status" - HTTP/1.1 200 OK - Content-Type: text/plain; charset=UTF-8 - Content-Length: 2 - Date: Fri, 11 Sep 2015 18:55:43 GMT - - OK + * Load balancers can 'ping' this url to determine service availability. + * Provides an endpoint that is similar to 'mod_status' in apache which + can provide details (or no details, depending on if configured) about + the activity of the server. + * *(and more)* + + Example requests/responses (**not** detailed mode):: + + $ curl -i -X HEAD "http://0.0.0.0:8775/healthcheck" + HTTP/1.1 204 No Content + Content-Type: text/plain; charset=UTF-8 + Content-Length: 0 + Date: Fri, 11 Sep 2015 18:55:08 GMT + + $ curl -i -X GET "http://0.0.0.0:8775/healthcheck" + HTTP/1.1 200 OK + Content-Type: text/plain; charset=UTF-8 + Content-Length: 2 + Date: Fri, 11 Sep 2015 18:55:43 GMT + + OK + + $ curl -X GET -i -H "Accept: application/json" "http://0.0.0.0:8775/healthcheck" + HTTP/1.0 200 OK + Date: Wed, 24 Aug 2016 06:09:58 GMT + Content-Type: application/json + Content-Length: 63 + + { + "detailed": false, + "reasons": [ + "OK" + ] + } + + $ curl -X GET -i -H "Accept: text/html" "http://0.0.0.0:8775/healthcheck" + HTTP/1.0 200 OK + Date: Wed, 24 Aug 2016 06:10:42 GMT + Content-Type: text/html; charset=UTF-8 + Content-Length: 239 + + <HTML> + <HEAD><TITLE>Healthcheck Status</TITLE></HEAD> + <BODY> + + <H2>Result of 1 checks:</H2> + <TABLE bgcolor="#ffffff" border="1"> + <TBODY> + <TR> + + <TH> + Reason + </TH> + </TR> + <TR> + <TD>OK</TD> + </TR> + </TBODY> + </TABLE> + <HR></HR> + + </BODY> + + Example requests/responses (**detailed** mode):: + + $ curl -X GET -i -H "Accept: application/json" "http://0.0.0.0:8775/healthcheck" + HTTP/1.0 200 OK + Date: Wed, 24 Aug 2016 06:11:59 GMT + Content-Type: application/json + Content-Length: 3480 + + { + "detailed": true, + "gc": { + "counts": [ + 293, + 10, + 5 + ], + "threshold": [ + 700, + 10, + 10 + ] + }, + "greenthreads": [ + ... + ], + "now": "2016-08-24 06:11:59.419267", + "platform": "Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty", + "python_version": "2.7.6 (default, Jun 22 2015, 17:58:13) \\n[GCC 4.8.2]", + "reasons": [ + { + "class": "HealthcheckResult", + "details": "Path '/tmp/dead' was not found", + "reason": "OK" + } + ], + "threads": [ + ... + ] + } + + $ curl -X GET -i -H "Accept: text/html" "http://0.0.0.0:8775/healthcheck" + HTTP/1.0 200 OK + Date: Wed, 24 Aug 2016 06:36:07 GMT + Content-Type: text/html; charset=UTF-8 + Content-Length: 6838 + + <HTML> + <HEAD><TITLE>Healthcheck Status</TITLE></HEAD> + <BODY> + <H1>Server status</H1> + <B>Server hostname:</B><PRE>...</PRE> + <B>Current time:</B><PRE>2016-08-24 06:36:07.302559</PRE> + <B>Python version:</B><PRE>2.7.6 (default, Jun 22 2015, 17:58:13) + [GCC 4.8.2]</PRE> + <B>Platform:</B><PRE>Linux-4.2.0-27-generic-x86_64-with-Ubuntu-14.04-trusty</PRE> + <HR></HR> + <H2>Garbage collector:</H2> + <B>Counts:</B><PRE>(77, 1, 6)</PRE> + <B>Thresholds:</B><PRE>(700, 10, 10)</PRE> + + <HR></HR> + <H2>Result of 1 checks:</H2> + <TABLE bgcolor="#ffffff" border="1"> + <TBODY> + <TR> + <TH> + Kind + </TH> + <TH> + Reason + </TH> + <TH> + Details + </TH> + + </TR> + <TR> + <TD>HealthcheckResult</TD> + <TD>OK</TD> + <TD>Path '/tmp/dead' was not found</TD> + </TR> + </TBODY> + </TABLE> + <HR></HR> + <H2>1 greenthread(s) active:</H2> + <TABLE bgcolor="#ffffff" border="1"> + <TBODY> + <TR> + <TD><PRE> File "oslo_middleware/healthcheck/__main__.py", line 94, in <module> + main() + File "oslo_middleware/healthcheck/__main__.py", line 90, in main + server.serve_forever() + ... + </PRE></TD> + </TR> + </TBODY> + </TABLE> + <HR></HR> + <H2>1 thread(s) active:</H2> + <TABLE bgcolor="#ffffff" border="1"> + <TBODY> + <TR> + <TD><PRE> File "oslo_middleware/healthcheck/__main__.py", line 94, in <module> + main() + File "oslo_middleware/healthcheck/__main__.py", line 90, in main + server.serve_forever() + .... + </TR> + </TBODY> + </TABLE> + </BODY> + </HTML> Example of paste configuration: @@ -92,7 +247,6 @@ class Healthcheck(base.ConfigurableMiddleware): [pipeline:public_api] pipeline = healthcheck sizelimit [...] public_service - Multiple filter sections can be defined if it desired to have pipelines with different healthcheck configuration, example: @@ -118,7 +272,6 @@ class Healthcheck(base.ConfigurableMiddleware): More details on available backends and their configuration can be found on this page: :doc:`healthcheck_plugins`. - """ NAMESPACE = "oslo.middleware.healthcheck" diff --git a/oslo_middleware/healthcheck/__main__.py b/oslo_middleware/healthcheck/__main__.py new file mode 100644 index 0000000..217fff6 --- /dev/null +++ b/oslo_middleware/healthcheck/__main__.py @@ -0,0 +1,69 @@ +# +# 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 argparse + +from six.moves import SimpleHTTPServer # noqa +from six.moves import socketserver +import webob + +from oslo_middleware import healthcheck + + +class HttpHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + def do_GET(self): + @webob.dec.wsgify + def dummy_application(req): + return 'test' + app = healthcheck.Healthcheck(dummy_application, {'detailed': True}) + req = webob.Request.blank("/healthcheck", accept='text/html', + method='GET') + res = req.get_response(app) + self.send_response(res.status_code) + for header_name, header_value in res.headerlist: + self.send_header(header_name, header_value) + self.end_headers() + self.wfile.write(res.body) + self.wfile.close() + + +def positive_int(blob): + value = int(blob) + if value < 0: + msg = "%r is not a positive integer" % blob + raise argparse.ArgumentTypeError(msg) + return value + + +def create_server(port=0): + handler = HttpHandler + server = socketserver.TCPServer(("", port), handler) + return server + + +def main(args=None): + """Runs a basic http server to show healthcheck functionality.""" + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", + help="Unused port to run the tiny" + " http server on (or zero to select a" + " random unused port)", + type=positive_int, required=True) + args = parser.parse_args(args=args) + server = create_server(args.port) + print("Serving at port: %s" % server.server_address[1]) + server.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/oslo_middleware/healthcheck/disable_by_file.py b/oslo_middleware/healthcheck/disable_by_file.py index 8a0403b..7fbb14b 100644 --- a/oslo_middleware/healthcheck/disable_by_file.py +++ b/oslo_middleware/healthcheck/disable_by_file.py @@ -52,7 +52,9 @@ class DisableByFilesPortsHealthcheck(pluginbase.HealthcheckBaseExtension): for port_path in paths.split(","): port_path = port_path.strip() if port_path: - port, path = port_path.split(":") + # On windows, drive letters are followed by colons, + # which makes split() return 3 elements in this case + port, path = port_path.split(":", 1) port = int(port) yield (port, path) diff --git a/oslo_middleware/http_proxy_to_wsgi.py b/oslo_middleware/http_proxy_to_wsgi.py index ad61401..4d68bcf 100644 --- a/oslo_middleware/http_proxy_to_wsgi.py +++ b/oslo_middleware/http_proxy_to_wsgi.py @@ -19,7 +19,7 @@ from oslo_middleware import base OPTS = [ cfg.BoolOpt('enable_proxy_headers_parsing', default=False, - help="Wether the application is behind a proxy or not. " + help="Whether the application is behind a proxy or not. " "This determines if the middleware should parse the " "headers or not.") ] @@ -71,6 +71,10 @@ class HTTPProxyToWSGI(base.ConfigurableMiddleware): if forwarded_host: req.environ['HTTP_HOST'] = forwarded_host + forwarded_for = proxy.get("for") + if forwarded_for: + req.environ['REMOTE_ADDR'] = forwarded_for + else: # World before RFC7239 forwarded_proto = req.environ.get("HTTP_X_FORWARDED_PROTO") @@ -81,6 +85,10 @@ class HTTPProxyToWSGI(base.ConfigurableMiddleware): if forwarded_host: req.environ['HTTP_HOST'] = forwarded_host + forwarded_for = req.environ.get("HTTP_X_FORWARDED_FOR") + if forwarded_for: + req.environ['REMOTE_ADDR'] = forwarded_for + v = req.environ.get("HTTP_X_FORWARDED_PREFIX") if v: req.environ['SCRIPT_NAME'] = v + req.environ['SCRIPT_NAME'] diff --git a/oslo_middleware/locale/oslo_middleware-log-error.pot b/oslo_middleware/locale/oslo_middleware-log-error.pot deleted file mode 100644 index 6df63c0..0000000 --- a/oslo_middleware/locale/oslo_middleware-log-error.pot +++ /dev/null @@ -1,25 +0,0 @@ -# Translations template for oslo.middleware. -# Copyright (C) 2016 ORGANIZATION -# This file is distributed under the same license as the oslo.middleware -# project. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2016. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: oslo.middleware 3.7.1.dev18\n" -"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2016-04-21 06:02+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.2.0\n" - -#: oslo_middleware/catch_errors.py:40 -#, python-format -msgid "An error occurred during processing the request: %s" -msgstr "" - diff --git a/oslo_middleware/locale/oslo_middleware.pot b/oslo_middleware/locale/oslo_middleware.pot deleted file mode 100644 index 009ca69..0000000 --- a/oslo_middleware/locale/oslo_middleware.pot +++ /dev/null @@ -1,25 +0,0 @@ -# Translations template for oslo.middleware. -# Copyright (C) 2016 ORGANIZATION -# This file is distributed under the same license as the oslo.middleware -# project. -# FIRST AUTHOR <EMAIL@ADDRESS>, 2016. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: oslo.middleware 3.7.1.dev18\n" -"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2016-04-21 06:02+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.2.0\n" - -#: oslo_middleware/sizelimit.py:59 oslo_middleware/sizelimit.py:73 -#: oslo_middleware/sizelimit.py:90 -msgid "Request is too large." -msgstr "" - diff --git a/oslo_middleware/opts.py b/oslo_middleware/opts.py index 1fe8c89..e66e723 100644 --- a/oslo_middleware/opts.py +++ b/oslo_middleware/opts.py @@ -17,7 +17,8 @@ __all__ = [ 'list_opts', 'list_opts_sizelimit', 'list_opts_ssl', - 'list_opts_cors' + 'list_opts_cors', + 'list_opts_http_proxy_to_wsgi', ] @@ -25,6 +26,7 @@ import copy import itertools from oslo_middleware import cors +from oslo_middleware import http_proxy_to_wsgi from oslo_middleware import sizelimit from oslo_middleware import ssl @@ -53,6 +55,7 @@ def list_opts(): list_opts_sizelimit(), list_opts_ssl(), list_opts_cors(), + list_opts_http_proxy_to_wsgi(), ) ) @@ -128,3 +131,27 @@ def list_opts_cors(): ('cors', copy.deepcopy(cors.CORS_OPTS)), ('cors.subdomain', copy.deepcopy(cors.CORS_OPTS)) ] + + +def list_opts_http_proxy_to_wsgi(): + """Return a list of oslo.config options for http_proxy_to_wsgi. + + The returned list includes all oslo.config options which may be registered + at runtime by the library. + + Each element of the list is a tuple. The first element is the name of the + group under which the list of elements in the second element will be + registered. A group name of None corresponds to the [DEFAULT] group in + config files. + + This function is also discoverable via the 'oslo.middleware' entry point + under the 'oslo.config.opts' namespace. + + The purpose of this is to allow tools like the Oslo sample config file + generator to discover the options exposed to users by this library. + + :returns: a list of (group_name, opts) tuples + """ + return [ + ('oslo_middleware', copy.deepcopy(http_proxy_to_wsgi.OPTS)), + ] diff --git a/oslo_middleware/ssl.py b/oslo_middleware/ssl.py index 7853dc1..f90e954 100644 --- a/oslo_middleware/ssl.py +++ b/oslo_middleware/ssl.py @@ -20,7 +20,7 @@ OPTS = [ deprecated_for_removal=True, help="The HTTP Header that will be used to determine what " "the original request protocol scheme was, even if it was " - "hidden by an SSL termination proxy.") + "hidden by a SSL termination proxy.") ] diff --git a/oslo_middleware/tests/test_base.py b/oslo_middleware/tests/test_base.py index ad48da5..57eee94 100644 --- a/oslo_middleware/tests/test_base.py +++ b/oslo_middleware/tests/test_base.py @@ -58,15 +58,26 @@ class TestBase(BaseTestCase): self.assertTrue(self.application.called_without_request) + def test_no_content_type_added(self): + class TestMiddleware(Middleware): + @staticmethod + def process_request(req): + return "foobar" + + m = TestMiddleware(None) + request = webob.Request({}, method='GET') + response = request.get_response(m) + self.assertNotIn('Content-Type', response.headers) + def test_paste_deploy_legacy(self): app = LegacyMiddlewareTest.factory( {'global': True}, local=True)(application) - self.assertEqual(app.conf, {}) + self.assertEqual({}, app.conf) def test_paste_deploy_configurable(self): app = ConfigurableMiddlewareTest.factory( {'global': True}, local=True)(application) - self.assertEqual(app.conf, {'global': True, 'local': True}) + self.assertEqual({'global': True, 'local': True}, app.conf) class NoRequestBase(Middleware): diff --git a/oslo_middleware/tests/test_correlation_id.py b/oslo_middleware/tests/test_correlation_id.py index 6dde5d8..63177fa 100644 --- a/oslo_middleware/tests/test_correlation_id.py +++ b/oslo_middleware/tests/test_correlation_id.py @@ -40,7 +40,7 @@ class CorrelationIdTest(test_base.BaseTestCase): middleware = correlation_id.CorrelationId(app) middleware(req) - self.assertEqual(req.headers.get("X_CORRELATION_ID"), "fake_uuid") + self.assertEqual("fake_uuid", req.headers.get("X_CORRELATION_ID")) def test_process_request_should_not_regenerate_correlation_id(self): app = mock.Mock() @@ -50,4 +50,4 @@ class CorrelationIdTest(test_base.BaseTestCase): middleware = correlation_id.CorrelationId(app) middleware(req) - self.assertEqual(req.headers.get("X_CORRELATION_ID"), "correlation_id") + self.assertEqual("correlation_id", req.headers.get("X_CORRELATION_ID")) diff --git a/oslo_middleware/tests/test_cors.py b/oslo_middleware/tests/test_cors.py index 5e0822f..e8276f3 100644 --- a/oslo_middleware/tests/test_cors.py +++ b/oslo_middleware/tests/test_cors.py @@ -79,7 +79,7 @@ class CORSTestBase(test_base.BaseTestCase): """ # Assert response status. - self.assertEqual(response.status, status) + self.assertEqual(status, response.status) # Assert the Access-Control-Allow-Origin header. self.assertHeader(response, @@ -163,7 +163,7 @@ class CORSTestDefaultOverrides(CORSTestBase): for opt in cors.CORS_OPTS: if opt.dest in self.override_opts: - self.assertEqual(opt.default, self.override_opts[opt.dest]) + self.assertEqual(self.override_opts[opt.dest], opt.default) def test_invalid_default_option(self): """Assert that using set_defaults only permits valid options.""" @@ -183,39 +183,39 @@ class CORSTestDefaultOverrides(CORSTestBase): # Check the global configuration for expected values: gc = self.config.cors - self.assertEqual(gc.allowed_origin, ['http://valid.example.com']) - self.assertEqual(gc.allow_credentials, - self.override_opts['allow_credentials']) - self.assertEqual(gc.expose_headers, - self.override_opts['expose_headers']) - self.assertEqual(gc.max_age, 10) - self.assertEqual(gc.allow_methods, - self.override_opts['allow_methods']) - self.assertEqual(gc.allow_headers, - self.override_opts['allow_headers']) + self.assertEqual(['http://valid.example.com'], gc.allowed_origin) + self.assertEqual(self.override_opts['allow_credentials'], + gc.allow_credentials) + self.assertEqual(self.override_opts['expose_headers'], + gc.expose_headers) + self.assertEqual(10, gc.max_age) + self.assertEqual(self.override_opts['allow_methods'], + gc.allow_methods) + self.assertEqual(self.override_opts['allow_headers'], + gc.allow_headers) # Check the child configuration for expected values: cc = self.config['cors.override_creds'] - self.assertEqual(cc.allowed_origin, ['http://creds.example.com']) + self.assertEqual(['http://creds.example.com'], cc.allowed_origin) self.assertTrue(cc.allow_credentials) - self.assertEqual(cc.expose_headers, - self.override_opts['expose_headers']) - self.assertEqual(cc.max_age, 10) - self.assertEqual(cc.allow_methods, - self.override_opts['allow_methods']) - self.assertEqual(cc.allow_headers, - self.override_opts['allow_headers']) + self.assertEqual(self.override_opts['expose_headers'], + cc.expose_headers) + self.assertEqual(10, cc.max_age) + self.assertEqual(self.override_opts['allow_methods'], + cc.allow_methods) + self.assertEqual(self.override_opts['allow_headers'], + cc.allow_headers) # Check the other child configuration for expected values: ec = self.config['cors.override_headers'] - self.assertEqual(ec.allowed_origin, ['http://headers.example.com']) - self.assertEqual(ec.allow_credentials, - self.override_opts['allow_credentials']) - self.assertEqual(ec.expose_headers, ['X-Header-1', 'X-Header-2']) - self.assertEqual(ec.max_age, 10) - self.assertEqual(ec.allow_methods, - self.override_opts['allow_methods']) - self.assertEqual(ec.allow_headers, ['X-Header-1', 'X-Header-2']) + self.assertEqual(['http://headers.example.com'], ec.allowed_origin) + self.assertEqual(self.override_opts['allow_credentials'], + ec.allow_credentials) + self.assertEqual(['X-Header-1', 'X-Header-2'], ec.expose_headers) + self.assertEqual(10, ec.max_age) + self.assertEqual(self.override_opts['allow_methods'], + ec.allow_methods) + self.assertEqual(['X-Header-1', 'X-Header-2'], ec.allow_headers) class CORSTestFilterFactory(CORSTestBase): @@ -358,68 +358,69 @@ class CORSRegularRequestTest(CORSTestBase): # Confirm global configuration gc = self.config.cors - self.assertEqual(gc.allowed_origin, ['http://valid.example.com']) - self.assertEqual(gc.allow_credentials, False) - self.assertEqual(gc.expose_headers, []) - self.assertEqual(gc.max_age, None) - self.assertEqual(gc.allow_methods, ['GET']) - self.assertEqual(gc.allow_headers, []) + self.assertEqual(['http://valid.example.com'], gc.allowed_origin) + self.assertEqual(False, gc.allow_credentials) + self.assertEqual([], gc.expose_headers) + self.assertEqual(None, gc.max_age) + self.assertEqual(['GET'], gc.allow_methods) + self.assertEqual([], gc.allow_headers) # Confirm credentials overrides. cc = self.config['cors.credentials'] - self.assertEqual(cc.allowed_origin, ['http://creds.example.com']) - self.assertEqual(cc.allow_credentials, True) - self.assertEqual(cc.expose_headers, gc.expose_headers) - self.assertEqual(cc.max_age, gc.max_age) - self.assertEqual(cc.allow_methods, gc.allow_methods) - self.assertEqual(cc.allow_headers, gc.allow_headers) + self.assertEqual(['http://creds.example.com'], cc.allowed_origin) + self.assertEqual(True, cc.allow_credentials) + self.assertEqual(gc.expose_headers, cc.expose_headers) + self.assertEqual(gc.max_age, cc.max_age) + self.assertEqual(gc.allow_methods, cc.allow_methods) + self.assertEqual(gc.allow_headers, cc.allow_headers) # Confirm exposed-headers overrides. ec = self.config['cors.exposed-headers'] - self.assertEqual(ec.allowed_origin, ['http://headers.example.com']) - self.assertEqual(ec.allow_credentials, gc.allow_credentials) - self.assertEqual(ec.expose_headers, ['X-Header-1', 'X-Header-2']) - self.assertEqual(ec.max_age, gc.max_age) - self.assertEqual(ec.allow_methods, gc.allow_methods) - self.assertEqual(ec.allow_headers, ['X-Header-1', 'X-Header-2']) + self.assertEqual(['http://headers.example.com'], ec.allowed_origin) + self.assertEqual(gc.allow_credentials, ec.allow_credentials) + self.assertEqual(['X-Header-1', 'X-Header-2'], ec.expose_headers) + self.assertEqual(gc.max_age, ec.max_age) + self.assertEqual(gc.allow_methods, ec.allow_methods) + self.assertEqual(['X-Header-1', 'X-Header-2'], ec.allow_headers) # Confirm cached overrides. chc = self.config['cors.cached'] - self.assertEqual(chc.allowed_origin, ['http://cached.example.com']) - self.assertEqual(chc.allow_credentials, gc.allow_credentials) - self.assertEqual(chc.expose_headers, gc.expose_headers) - self.assertEqual(chc.max_age, 3600) - self.assertEqual(chc.allow_methods, gc.allow_methods) - self.assertEqual(chc.allow_headers, gc.allow_headers) + self.assertEqual(['http://cached.example.com'], chc.allowed_origin) + self.assertEqual(gc.allow_credentials, chc.allow_credentials) + self.assertEqual(gc.expose_headers, chc.expose_headers) + self.assertEqual(3600, chc.max_age) + self.assertEqual(gc.allow_methods, chc.allow_methods) + self.assertEqual(gc.allow_headers, chc.allow_headers) # Confirm get-only overrides. goc = self.config['cors.get-only'] - self.assertEqual(goc.allowed_origin, ['http://get.example.com']) - self.assertEqual(goc.allow_credentials, gc.allow_credentials) - self.assertEqual(goc.expose_headers, gc.expose_headers) - self.assertEqual(goc.max_age, gc.max_age) - self.assertEqual(goc.allow_methods, ['GET']) - self.assertEqual(goc.allow_headers, gc.allow_headers) + self.assertEqual(['http://get.example.com'], goc.allowed_origin) + self.assertEqual(gc.allow_credentials, goc.allow_credentials) + self.assertEqual(gc.expose_headers, goc.expose_headers) + self.assertEqual(gc.max_age, goc.max_age) + self.assertEqual(['GET'], goc.allow_methods) + self.assertEqual(gc.allow_headers, goc.allow_headers) # Confirm all-methods overrides. ac = self.config['cors.all-methods'] - self.assertEqual(ac.allowed_origin, ['http://all.example.com']) - self.assertEqual(ac.allow_credentials, gc.allow_credentials) - self.assertEqual(ac.expose_headers, gc.expose_headers) - self.assertEqual(ac.max_age, gc.max_age) - self.assertEqual(ac.allow_methods, - ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']) - self.assertEqual(ac.allow_headers, gc.allow_headers) + self.assertEqual(['http://all.example.com'], ac.allowed_origin) + self.assertEqual(gc.allow_credentials, ac.allow_credentials) + self.assertEqual(gc.expose_headers, ac.expose_headers) + self.assertEqual(gc.max_age, ac.max_age) + self.assertEqual(['GET', 'PUT', 'POST', 'DELETE', 'HEAD'], + ac.allow_methods) + self.assertEqual(gc.allow_headers, ac.allow_headers) # Confirm duplicate domains. ac = self.config['cors.duplicate'] - self.assertEqual(ac.allowed_origin, ['http://domain1.example.com', - 'http://domain2.example.com']) - self.assertEqual(ac.allow_credentials, gc.allow_credentials) - self.assertEqual(ac.expose_headers, gc.expose_headers) - self.assertEqual(ac.max_age, gc.max_age) - self.assertEqual(ac.allow_methods, gc.allow_methods) - self.assertEqual(ac.allow_headers, gc.allow_headers) + self.assertEqual(['http://domain1.example.com', + 'http://domain2.example.com'], + ac.allowed_origin) + self.assertEqual(gc.allow_credentials, ac.allow_credentials) + self.assertEqual(gc.expose_headers, ac.expose_headers) + self.assertEqual(gc.max_age, ac.max_age) + self.assertEqual(gc.allow_methods, ac.allow_methods) + self.assertEqual(gc.allow_headers, ac.allow_headers) def test_no_origin_header(self): """CORS Specification Section 6.1.1 @@ -683,49 +684,49 @@ class CORSPreflightRequestTest(CORSTestBase): # Confirm credentials overrides. cc = self.config['cors.credentials'] - self.assertEqual(cc.allowed_origin, ['http://creds.example.com']) - self.assertEqual(cc.allow_credentials, True) - self.assertEqual(cc.expose_headers, gc.expose_headers) - self.assertEqual(cc.max_age, gc.max_age) - self.assertEqual(cc.allow_methods, gc.allow_methods) - self.assertEqual(cc.allow_headers, gc.allow_headers) + self.assertEqual(['http://creds.example.com'], cc.allowed_origin) + self.assertEqual(True, cc.allow_credentials) + self.assertEqual(gc.expose_headers, cc.expose_headers) + self.assertEqual(gc.max_age, cc.max_age) + self.assertEqual(gc.allow_methods, cc.allow_methods) + self.assertEqual(gc.allow_headers, cc.allow_headers) # Confirm exposed-headers overrides. ec = self.config['cors.exposed-headers'] - self.assertEqual(ec.allowed_origin, ['http://headers.example.com']) - self.assertEqual(ec.allow_credentials, gc.allow_credentials) - self.assertEqual(ec.expose_headers, ['X-Header-1', 'X-Header-2']) - self.assertEqual(ec.max_age, gc.max_age) - self.assertEqual(ec.allow_methods, gc.allow_methods) - self.assertEqual(ec.allow_headers, ['X-Header-1', 'X-Header-2']) + self.assertEqual(['http://headers.example.com'], ec.allowed_origin) + self.assertEqual(gc.allow_credentials, ec.allow_credentials) + self.assertEqual(['X-Header-1', 'X-Header-2'], ec.expose_headers) + self.assertEqual(gc.max_age, ec.max_age) + self.assertEqual(gc.allow_methods, ec.allow_methods) + self.assertEqual(['X-Header-1', 'X-Header-2'], ec.allow_headers) # Confirm cached overrides. chc = self.config['cors.cached'] - self.assertEqual(chc.allowed_origin, ['http://cached.example.com']) - self.assertEqual(chc.allow_credentials, gc.allow_credentials) - self.assertEqual(chc.expose_headers, gc.expose_headers) - self.assertEqual(chc.max_age, 3600) - self.assertEqual(chc.allow_methods, gc.allow_methods) - self.assertEqual(chc.allow_headers, gc.allow_headers) + self.assertEqual(['http://cached.example.com'], chc.allowed_origin) + self.assertEqual(gc.allow_credentials, chc.allow_credentials) + self.assertEqual(gc.expose_headers, chc.expose_headers) + self.assertEqual(3600, chc.max_age) + self.assertEqual(gc.allow_methods, chc.allow_methods) + self.assertEqual(gc.allow_headers, chc.allow_headers) # Confirm get-only overrides. goc = self.config['cors.get-only'] - self.assertEqual(goc.allowed_origin, ['http://get.example.com']) - self.assertEqual(goc.allow_credentials, gc.allow_credentials) - self.assertEqual(goc.expose_headers, gc.expose_headers) - self.assertEqual(goc.max_age, gc.max_age) - self.assertEqual(goc.allow_methods, ['GET']) - self.assertEqual(goc.allow_headers, gc.allow_headers) + self.assertEqual(['http://get.example.com'], goc.allowed_origin) + self.assertEqual(gc.allow_credentials, goc.allow_credentials) + self.assertEqual(gc.expose_headers, goc.expose_headers) + self.assertEqual(gc.max_age, goc.max_age) + self.assertEqual(['GET'], goc.allow_methods) + self.assertEqual(gc.allow_headers, goc.allow_headers) # Confirm all-methods overrides. ac = self.config['cors.all-methods'] - self.assertEqual(ac.allowed_origin, ['http://all.example.com']) - self.assertEqual(ac.allow_credentials, gc.allow_credentials) - self.assertEqual(ac.expose_headers, gc.expose_headers) - self.assertEqual(ac.max_age, gc.max_age) + self.assertEqual(['http://all.example.com'], ac.allowed_origin) + self.assertEqual(gc.allow_credentials, ac.allow_credentials) + self.assertEqual(gc.expose_headers, ac.expose_headers) + self.assertEqual(gc.max_age, ac.max_age) self.assertEqual(ac.allow_methods, ['GET', 'PUT', 'POST', 'DELETE', 'HEAD']) - self.assertEqual(ac.allow_headers, gc.allow_headers) + self.assertEqual(gc.allow_headers, ac.allow_headers) def test_no_origin_header(self): """CORS Specification Section 6.2.1 @@ -1141,10 +1142,10 @@ class CORSPreflightRequestTest(CORSTestBase): # If the regular CORS handling catches this request, it should set # the allow credentials header. This makes sure that it doesn't. self.assertNotIn('Access-Control-Allow-Credentials', response.headers) - self.assertEqual(response.headers['Access-Control-Allow-Origin'], - test_origin) - self.assertEqual(response.headers['X-Server-Generated-Response'], - '1') + self.assertEqual(test_origin, + response.headers['Access-Control-Allow-Origin']) + self.assertEqual('1', + response.headers['X-Server-Generated-Response']) # If the application returns an OPTIONS response without CORS # headers, assert that we apply headers. @@ -1191,22 +1192,22 @@ class CORSTestWildcard(CORSTestBase): # Confirm global configuration gc = self.config.cors - self.assertEqual(gc.allowed_origin, ['http://default.example.com']) - self.assertEqual(gc.allow_credentials, True) - self.assertEqual(gc.expose_headers, []) - self.assertEqual(gc.max_age, None) - self.assertEqual(gc.allow_methods, ['GET', 'PUT', 'POST', 'DELETE', - 'HEAD']) - self.assertEqual(gc.allow_headers, []) + self.assertEqual(['http://default.example.com'], gc.allowed_origin) + self.assertEqual(True, gc.allow_credentials) + self.assertEqual([], gc.expose_headers) + self.assertEqual(None, gc.max_age) + self.assertEqual(['GET', 'PUT', 'POST', 'DELETE', 'HEAD'], + gc.allow_methods) + self.assertEqual([], gc.allow_headers) # Confirm all-methods overrides. ac = self.config['cors.wildcard'] - self.assertEqual(ac.allowed_origin, ['*']) - self.assertEqual(gc.allow_credentials, True) - self.assertEqual(ac.expose_headers, gc.expose_headers) - self.assertEqual(ac.max_age, gc.max_age) - self.assertEqual(ac.allow_methods, ['GET']) - self.assertEqual(ac.allow_headers, gc.allow_headers) + self.assertEqual(['*'], ac.allowed_origin) + self.assertEqual(True, gc.allow_credentials) + self.assertEqual(gc.expose_headers, ac.expose_headers) + self.assertEqual(gc.max_age, ac.max_age) + self.assertEqual(['GET'], ac.allow_methods) + self.assertEqual(gc.allow_headers, ac.allow_headers) def test_wildcard_domain(self): """CORS Specification, Wildcards diff --git a/oslo_middleware/tests/test_healthcheck.py b/oslo_middleware/tests/test_healthcheck.py index 3ffb03f..df73969 100644 --- a/oslo_middleware/tests/test_healthcheck.py +++ b/oslo_middleware/tests/test_healthcheck.py @@ -13,12 +13,39 @@ # License for the specific language governing permissions and limitations # under the License. +import threading +import time + import mock from oslotest import base as test_base +import requests import webob.dec import webob.exc from oslo_middleware import healthcheck +from oslo_middleware.healthcheck import __main__ + + +class HealthcheckMainTests(test_base.BaseTestCase): + + def test_startup_response(self): + server = __main__.create_server(0) + th = threading.Thread(target=server.serve_forever) + th.start() + self.addCleanup(server.shutdown) + while True: + try: + # Connecting on 0.0.0.0 is not allowed on windows + # The operating system will return WSAEADDRNOTAVAIL which + # in turn will throw a requests.ConnectionError + r = requests.get("http://127.0.0.1:%s" % ( + server.server_address[1])) + except requests.ConnectionError: + # Server hasn't started up yet, try again in a few. + time.sleep(1) + else: + self.assertEqual(200, r.status_code) + break class HealthcheckTests(test_base.BaseTestCase): diff --git a/oslo_middleware/tests/test_http_proxy_to_wsgi.py b/oslo_middleware/tests/test_http_proxy_to_wsgi.py index 26baa77..1554ece 100644 --- a/oslo_middleware/tests/test_http_proxy_to_wsgi.py +++ b/oslo_middleware/tests/test_http_proxy_to_wsgi.py @@ -103,6 +103,29 @@ class TestHTTPProxyToWSGI(test_base.BaseTestCase): response = self.request.get_response(self.middleware) self.assertEqual(b"https://example.com:8043/bla", response.body) + def test_forwarded_for_headers(self): + @webob.dec.wsgify() + def fake_app(req): + return req.environ['REMOTE_ADDR'] + + self.middleware = http_proxy_to_wsgi.HTTPProxyToWSGIMiddleware( + fake_app) + forwarded_for_addr = '1.2.3.4' + forwarded_addr = '8.8.8.8' + + # If both X-Forwarded-For and Fowarded headers are present, it should + # use the Forwarded header and ignore the X-Forwarded-For header. + self.request.headers['Forwarded'] = ( + "for=%s;proto=https;host=example.com:8043" % (forwarded_addr)) + self.request.headers['X-Forwarded-For'] = forwarded_for_addr + response = self.request.get_response(self.middleware) + self.assertEqual(forwarded_addr.encode(), response.body) + + # Now if only X-Forwarded-For header is present, it should be used. + del self.request.headers['Forwarded'] + response = self.request.get_response(self.middleware) + self.assertEqual(forwarded_for_addr.encode(), response.body) + class TestHTTPProxyToWSGIDisabled(test_base.BaseTestCase): diff --git a/oslo_middleware/tests/test_request_id.py b/oslo_middleware/tests/test_request_id.py index 76f3696..039b7af 100644 --- a/oslo_middleware/tests/test_request_id.py +++ b/oslo_middleware/tests/test_request_id.py @@ -36,4 +36,4 @@ class RequestIdTest(test_base.BaseTestCase): res_req_id = res_req_id.decode('utf-8') self.assertThat(res_req_id, matchers.StartsWith('req-')) # request-id in request environ is returned as response body - self.assertEqual(res_req_id, res.body.decode('utf-8')) + self.assertEqual(res.body.decode('utf-8'), res_req_id) diff --git a/oslo_middleware/tests/test_sizelimit.py b/oslo_middleware/tests/test_sizelimit.py index dc29cd0..42622ae 100644 --- a/oslo_middleware/tests/test_sizelimit.py +++ b/oslo_middleware/tests/test_sizelimit.py @@ -29,7 +29,7 @@ class TestLimitingReader(test_base.BaseTestCase): for chunk in sizelimit.LimitingReader(data, BYTES): bytes_read += len(chunk) - self.assertEqual(bytes_read, BYTES) + self.assertEqual(BYTES, bytes_read) bytes_read = 0 data = six.StringIO("*" * BYTES) @@ -39,7 +39,7 @@ class TestLimitingReader(test_base.BaseTestCase): bytes_read += 1 byte = reader.read(1) - self.assertEqual(bytes_read, BYTES) + self.assertEqual(BYTES, bytes_read) def test_read_default_value(self): BYTES = 1024 @@ -93,16 +93,16 @@ class TestRequestBodySizeLimiter(test_base.BaseTestCase): self.request.headers['Content-Length'] = self.MAX_REQUEST_BODY_SIZE self.request.body = b"0" * self.MAX_REQUEST_BODY_SIZE response = self.request.get_response(self.middleware) - self.assertEqual(response.status_int, 200) + self.assertEqual(200, response.status_int) def test_content_length_too_large(self): self.request.headers['Content-Length'] = self.MAX_REQUEST_BODY_SIZE + 1 self.request.body = b"0" * (self.MAX_REQUEST_BODY_SIZE + 1) response = self.request.get_response(self.middleware) - self.assertEqual(response.status_int, 413) + self.assertEqual(413, response.status_int) def test_request_too_large_no_content_length(self): self.request.body = b"0" * (self.MAX_REQUEST_BODY_SIZE + 1) self.request.headers['Content-Length'] = None response = self.request.get_response(self.middleware) - self.assertEqual(response.status_int, 413) + self.assertEqual(413, response.status_int) diff --git a/oslo_middleware/version.py b/oslo_middleware/version.py new file mode 100644 index 0000000..f73224f --- /dev/null +++ b/oslo_middleware/version.py @@ -0,0 +1,18 @@ +# Copyright 2016 OpenStack Foundation +# +# 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_info = pbr.version.VersionInfo('oslo_middleware') diff --git a/releasenotes/notes/add_reno-3b4ae0789e9c45b4.yaml b/releasenotes/notes/add_reno-3b4ae0789e9c45b4.yaml new file mode 100644 index 0000000..46a2da6 --- /dev/null +++ b/releasenotes/notes/add_reno-3b4ae0789e9c45b4.yaml @@ -0,0 +1,3 @@ +--- +other: + - Switch to reno for managing release notes.
\ No newline at end of file diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/releasenotes/source/_static/.placeholder diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/releasenotes/source/_templates/.placeholder diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..4bbee08 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,273 @@ +# -*- 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. +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'oslo.middleware Release Notes' +copyright = u'2016, oslo.middleware Developers' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +from oslo_middleware.version import version_info as oslo_middleware_version + +# The full version, including alpha/beta/rc tags. +release = oslo_middleware_version.version_string_with_vcs() +# The short X.Y version. +version = oslo_middleware_version.canonical_version_string() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# 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 + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'oslo.middlewareReleaseNotesDoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'oslo.middlewareReleaseNotes.tex', + u'oslo.middleware Release Notes Documentation', + u'oslo.middleware Developers', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'oslo.middlewareReleaseNotes', + u'oslo.middleware Release Notes Documentation', + [u'oslo.middleware Developers'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'oslo.middlewareReleaseNotes', + u'oslo.middleware Release Notes Documentation', + u'oslo.middleware Developers', 'oslo.middlewareReleaseNotes', + 'The library includes components that can be injected into wsgi pipelines' + ' to intercept request/response flows.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..d19561d --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,8 @@ +============================= +oslo.middleware Release Notes +============================= + + .. toctree:: + :maxdepth: 1 + + unreleased diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..5860a46 --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +========================== + Unreleased Release Notes +========================== + +.. release-notes:: diff --git a/requirements.txt b/requirements.txt index f03122f..db1bfbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,11 @@ pbr>=1.6 # Apache-2.0 Jinja2>=2.8 # BSD License (3 clause) -oslo.config>=3.9.0 # Apache-2.0 -oslo.context>=2.2.0 # Apache-2.0 +oslo.config>=3.14.0 # Apache-2.0 +oslo.context>=2.9.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 -oslo.utils>=3.5.0 # Apache-2.0 +oslo.utils>=3.16.0 # Apache-2.0 six>=1.9.0 # MIT -stevedore>=1.9.0 # Apache-2.0 -WebOb>=1.2.3 # MIT +stevedore>=1.17.1 # Apache-2.0 +WebOb>=1.6.0 # MIT debtcollector>=1.2.0 # Apache-2.0 @@ -28,6 +28,7 @@ oslo.config.opts = oslo.middleware.cors = oslo_middleware.opts:list_opts_cors oslo.middleware.sizelimit = oslo_middleware.opts:list_opts_sizelimit oslo.middleware.ssl = oslo_middleware.opts:list_opts_ssl + oslo.middleware.http_proxy_to_wsgi = oslo_middleware.opts:list_opts_http_proxy_to_wsgi oslo.middleware.healthcheck = disable_by_file = oslo_middleware.healthcheck.disable_by_file:DisableByFileHealthcheck diff --git a/test-requirements.txt b/test-requirements.txt index fc60e42..b241c09 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,11 +2,12 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -fixtures<2.0,>=1.3.1 # Apache-2.0/BSD +fixtures>=3.0.0 # Apache-2.0/BSD hacking<0.11,>=0.10.0 -mock>=1.2 # BSD -oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 +mock>=2.0 # BSD +oslosphinx>=4.7.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 -sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD +sphinx!=1.3b1,<1.4,>=1.2.1 # BSD testtools>=1.4.0 # MIT coverage>=3.6 # Apache-2.0 +reno>=1.8.0 # Apache2 @@ -35,3 +35,6 @@ import_exceptions = oslo_middleware._i18n # of the requirements.txt files deps = pip_missing_reqs commands = pip-missing-reqs -d --ignore-module=oslo_middleware* --ignore-module=pkg_resources --ignore-file=oslo_middleware/tests/* oslo_middleware + +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html |