Coverage for manila/api/openstack/__init__.py: 86%
82 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2013 OpenStack, LLC.
2#
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
17"""
18WSGI middleware for OpenStack API controllers.
19"""
21from oslo_config import cfg
22from oslo_log import log
23from oslo_service import wsgi as base_wsgi
24import routes
26from manila.api.openstack import wsgi
27from manila.i18n import _
29openstack_api_opts = [
30 cfg.StrOpt(
31 'project_id_regex',
32 default=r'[0-9a-f\-]+',
33 help=(
34 r'The validation regex for project_ids used in URLs. '
35 r'This defaults to [0-9a-f\\-]+ if not set, '
36 r'which matches normal uuids created by keystone.'
37 ),
38 ),
39]
40validation_opts = [
41 cfg.StrOpt(
42 'response_validation',
43 choices=(
44 (
45 'error',
46 'Raise a HTTP 500 (Server Error) for responses that fail '
47 'schema validation',
48 ),
49 (
50 'warn',
51 'Log a warning for responses that fail schema validation',
52 ),
53 (
54 'ignore',
55 'Ignore schema validation failures',
56 ),
57 ),
58 default='warn',
59 help="""\
60Configure validation of API responses.
62``warn`` is the current recommendation for production environments. If you find
63it necessary to enable the ``ignore`` option, please report the issues you are
64seeing to the Manila team so we can improve our schemas.
66``error`` should not be used in a production environment. This is because
67schema validation happens *after* the response body has been generated, meaning
68any side effects will still happen and the call may be non-idempotent despite
69the user receiving a HTTP 500 error.
70""",
71 ),
72]
74CONF = cfg.CONF
75CONF.register_opts(openstack_api_opts)
76CONF.register_opts(validation_opts, group='api')
77LOG = log.getLogger(__name__)
80class APIMapper(routes.Mapper):
81 def routematch(self, url=None, environ=None):
82 if url == "": 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true
83 result = self._match("", environ)
84 return result[0], result[1]
85 return routes.Mapper.routematch(self, url, environ)
87 def connect(self, *args, **kwargs):
88 # NOTE(inhye): Default the format part of a route to only accept json
89 # and xml so it doesn't eat all characters after a '.'
90 # in the url.
91 kwargs.setdefault('requirements', {})
92 if not kwargs['requirements'].get('format'):
93 kwargs['requirements']['format'] = 'json|xml'
94 return routes.Mapper.connect(self, *args, **kwargs)
97class ProjectMapper(APIMapper):
98 def resource(self, member_name, collection_name, **kwargs):
99 """Base resource path handler
101 This method is compatible with resource paths that include a
102 project_id and those that don't. Including project_id in the URLs
103 was a legacy API requirement; and making API requests against
104 such endpoints won't work for users that don't belong to a
105 particular project.
106 """
107 # NOTE(gouthamr): project_id parameter is only valid if its hex
108 # or hex + dashes (note, integers are a subset of this). This
109 # is required to handle our overlapping routes issues.
110 project_id_regex = CONF.project_id_regex
111 project_id_token = '{project_id:%s}' % project_id_regex
112 if 'parent_resource' not in kwargs:
113 kwargs['path_prefix'] = '%s/' % project_id_token
114 else:
115 parent_resource = kwargs['parent_resource']
116 p_collection = parent_resource['collection_name']
117 p_member = parent_resource['member_name']
118 kwargs['path_prefix'] = '%s/%s/:%s_id' % (project_id_token,
119 p_collection,
120 p_member)
121 routes.Mapper.resource(self,
122 member_name,
123 collection_name,
124 **kwargs)
126 # NOTE(gouthamr): while we are in transition mode to not needing
127 # project_ids in URLs, we'll need additional routes without project_id.
128 if 'parent_resource' not in kwargs:
129 del kwargs['path_prefix']
130 else:
131 parent_resource = kwargs['parent_resource']
132 p_collection = parent_resource['collection_name']
133 p_member = parent_resource['member_name']
134 kwargs['path_prefix'] = '%s/:%s_id' % (p_collection,
135 p_member)
136 routes.Mapper.resource(self,
137 member_name,
138 collection_name,
139 **kwargs)
142class APIRouter(base_wsgi.Router):
143 """Routes requests on the API to the appropriate controller and method."""
144 ExtensionManager = None # override in subclasses
146 @classmethod
147 def factory(cls, global_config, **local_config):
148 """Simple paste factory, :class:`manila.wsgi.Router` doesn't have."""
149 return cls()
151 def __init__(self, ext_mgr=None):
152 if ext_mgr is None:
153 if self.ExtensionManager: 153 ↛ 157line 153 didn't jump to line 157 because the condition on line 153 was always true
154 # pylint: disable=not-callable
155 ext_mgr = self.ExtensionManager()
156 else:
157 raise Exception(_("Must specify an ExtensionManager class"))
159 mapper = ProjectMapper()
160 self.resources = {}
161 self._setup_routes(mapper)
162 self._setup_ext_routes(mapper, ext_mgr)
163 self._setup_extensions(ext_mgr)
164 super(APIRouter, self).__init__(mapper)
166 def _setup_ext_routes(self, mapper, ext_mgr):
167 for resource in ext_mgr.get_resources():
168 LOG.debug('Extended resource: %s',
169 resource.collection)
171 wsgi_resource = wsgi.Resource(resource.controller)
172 self.resources[resource.collection] = wsgi_resource
173 kargs = dict(
174 controller=wsgi_resource,
175 collection=resource.collection_actions,
176 member=resource.member_actions)
178 if resource.parent: 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true
179 kargs['parent_resource'] = resource.parent
181 mapper.resource(resource.collection, resource.collection, **kargs)
183 if resource.custom_routes_fn: 183 ↛ 184line 183 didn't jump to line 184 because the condition on line 183 was never true
184 resource.custom_routes_fn(mapper, wsgi_resource)
186 def _setup_extensions(self, ext_mgr):
187 for extension in ext_mgr.get_controller_extensions():
188 ext_name = extension.extension.name
189 collection = extension.collection
190 controller = extension.controller
192 if collection not in self.resources: 192 ↛ 198line 192 didn't jump to line 198 because the condition on line 192 was always true
193 LOG.warning('Extension %(ext_name)s: Cannot extend '
194 'resource %(collection)s: No such resource',
195 {'ext_name': ext_name, 'collection': collection})
196 continue
198 LOG.debug('Extension %(ext_name)s extending resource: '
199 '%(collection)s',
200 {'ext_name': ext_name, 'collection': collection})
202 resource = self.resources[collection]
203 resource.register_actions(controller)
204 resource.register_extensions(controller)
206 def _setup_routes(self, mapper):
207 raise NotImplementedError