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

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. 

16 

17""" 

18WSGI middleware for OpenStack API controllers. 

19""" 

20 

21from oslo_config import cfg 

22from oslo_log import log 

23from oslo_service import wsgi as base_wsgi 

24import routes 

25 

26from manila.api.openstack import wsgi 

27from manila.i18n import _ 

28 

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. 

61 

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. 

65 

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] 

73 

74CONF = cfg.CONF 

75CONF.register_opts(openstack_api_opts) 

76CONF.register_opts(validation_opts, group='api') 

77LOG = log.getLogger(__name__) 

78 

79 

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) 

86 

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) 

95 

96 

97class ProjectMapper(APIMapper): 

98 def resource(self, member_name, collection_name, **kwargs): 

99 """Base resource path handler 

100 

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) 

125 

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) 

140 

141 

142class APIRouter(base_wsgi.Router): 

143 """Routes requests on the API to the appropriate controller and method.""" 

144 ExtensionManager = None # override in subclasses 

145 

146 @classmethod 

147 def factory(cls, global_config, **local_config): 

148 """Simple paste factory, :class:`manila.wsgi.Router` doesn't have.""" 

149 return cls() 

150 

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")) 

158 

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) 

165 

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) 

170 

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) 

177 

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 

180 

181 mapper.resource(resource.collection, resource.collection, **kargs) 

182 

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) 

185 

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 

191 

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 

197 

198 LOG.debug('Extension %(ext_name)s extending resource: ' 

199 '%(collection)s', 

200 {'ext_name': ext_name, 'collection': collection}) 

201 

202 resource = self.resources[collection] 

203 resource.register_actions(controller) 

204 resource.register_extensions(controller) 

205 

206 def _setup_routes(self, mapper): 

207 raise NotImplementedError