diff --git a/etc/murano/murano-api-paste.ini b/etc/murano/murano-api-paste.ini index 08cf50e3..e8e59c57 100644 --- a/etc/murano/murano-api-paste.ini +++ b/etc/murano/murano-api-paste.ini @@ -1,8 +1,5 @@ [pipeline:muranoapi] -pipeline = authtoken context apiv1app - -[app:apiv1app] -paste.app_factory = muranoapi.api.v1.router:API.factory +pipeline = versionnegotiation authtoken context rootapp [filter:context] paste.filter_factory = muranoapi.api.middleware.context:ContextMiddleware.factory @@ -11,3 +8,17 @@ paste.filter_factory = muranoapi.api.middleware.context:ContextMiddleware.factor #http://docs.openstack.org/developer/keystone/configuringservices.html [filter:authtoken] paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory + +[composite:rootapp] +use = egg:Paste#urlmap +/: apiversions +/v1: apiv1app + +[app:apiversions] +paste.app_factory = muranoapi.api.versions:create_resource + +[app:apiv1app] +paste.app_factory = muranoapi.api.v1.router:API.factory + +[filter:versionnegotiation] +paste.filter_factory = muranoapi.api.middleware.version_negotiation:VersionNegotiationFilter.factory diff --git a/muranoapi/api/middleware/version_negotiation.py b/muranoapi/api/middleware/version_negotiation.py new file mode 100644 index 00000000..8215b812 --- /dev/null +++ b/muranoapi/api/middleware/version_negotiation.py @@ -0,0 +1,96 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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. + +""" +A filter middleware that inspects the requested URI for a version string +and/or Accept headers and attempts to negotiate an API controller to +return +""" + +from oslo.config import cfg + +from muranoapi.api import versions +from muranoapi.openstack.common.gettextutils import _ # noqa +import muranoapi.openstack.common.log as logging +from muranoapi.openstack.common import wsgi + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class VersionNegotiationFilter(wsgi.Middleware): + @classmethod + def factory(cls, global_conf, **local_conf): + def filter(app): + return cls(app) + return filter + + def __init__(self, app): + self.versions_app = versions.Controller() + super(VersionNegotiationFilter, self).__init__(app) + + def process_request(self, req): + """Try to find a version first in the accept header, then the URL""" + msg = _("Determining version of request: %(method)s %(path)s" + " Accept: %(accept)s") + args = {'method': req.method, 'path': req.path, 'accept': req.accept} + LOG.debug(msg % args) + + LOG.debug(_("Using url versioning")) + # Remove version in url so it doesn't conflict later + req_version = self._pop_path_info(req) + + try: + version = self._match_version_string(req_version) + except ValueError: + LOG.debug(_("Unknown version. Returning version choices.")) + return self.versions_app + + req.environ['api.version'] = version + req.path_info = ''.join(('/v', str(version), req.path_info)) + LOG.debug(_("Matched version: v%d"), version) + LOG.debug('new path %s' % req.path_info) + return None + + def _match_version_string(self, subject): + """ + Given a string, tries to match a major and/or + minor version number. + + :param subject: The string to check + :returns version found in the subject + :raises ValueError if no acceptable version could be found + """ + if subject in ('v1',): + major_version = 1 + else: + raise ValueError() + return major_version + + def _pop_path_info(self, req): + """ + 'Pops' off the next segment of PATH_INFO, returns the popped + segment. Do NOT push it onto SCRIPT_NAME. + """ + path = req.path_info + if not path: + return None + while path.startswith('/'): + path = path[1:] + idx = path.find('/') + if idx == -1: + idx = len(path) + r = path[:idx] + req.path_info = path[idx:] + return r diff --git a/muranoapi/api/v1/deployments.py b/muranoapi/api/v1/deployments.py index 4f4c0f3b..4d47dc82 100644 --- a/muranoapi/api/v1/deployments.py +++ b/muranoapi/api/v1/deployments.py @@ -19,7 +19,6 @@ from muranoapi.common import utils from muranoapi.db import models from muranoapi.db import session as db_session - from muranoapi.openstack.common.gettextutils import _ # noqa from muranoapi.openstack.common import log as logging from muranoapi.openstack.common import wsgi diff --git a/muranoapi/api/v1/environments.py b/muranoapi/api/v1/environments.py index 18cf8825..4bdf7459 100644 --- a/muranoapi/api/v1/environments.py +++ b/muranoapi/api/v1/environments.py @@ -15,7 +15,6 @@ from sqlalchemy import desc from webob import exc - from muranoapi.api.v1 import statistics from muranoapi.common import utils from muranoapi.db import models @@ -23,7 +22,6 @@ from muranoapi.db.services import core_services from muranoapi.db.services import environments as envs from muranoapi.db import session as db_session - from muranoapi.openstack.common.gettextutils import _ # noqa from muranoapi.openstack.common import log as logging from muranoapi.openstack.common import wsgi diff --git a/muranoapi/api/v1/router.py b/muranoapi/api/v1/router.py index 58f703b6..280fb5ad 100644 --- a/muranoapi/api/v1/router.py +++ b/muranoapi/api/v1/router.py @@ -11,7 +11,6 @@ # 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 routes from muranoapi.api.v1 import deployments diff --git a/muranoapi/api/versions.py b/muranoapi/api/versions.py new file mode 100644 index 00000000..c76b4076 --- /dev/null +++ b/muranoapi/api/versions.py @@ -0,0 +1,62 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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 httplib + +from oslo.config import cfg +import webob.dec + +from muranoapi.openstack.common import jsonutils +from muranoapi.openstack.common import wsgi + + +CONF = cfg.CONF + + +class Controller(object): + + """A wsgi controller that reports which API versions are supported.""" + + def index(self, req): + """Respond to a request for all OpenStack API versions.""" + def build_version_object(version, path, status): + return { + 'id': 'v%s' % version, + 'status': status, + 'links': [ + { + 'rel': 'self', + 'href': '%s/%s/' % (req.host_url, path), + }, + ], + } + + version_objs = [] + version_objs.extend([ + build_version_object(1.0, 'v1', 'CURRENT'), + ]) + + response = webob.Response(request=req, + status=httplib.MULTIPLE_CHOICES, + content_type='application/json') + response.body = jsonutils.dumps(dict(versions=version_objs)) + return response + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + return self.index(req) + + +def create_resource(conf): + return wsgi.Resource(Controller())