OpenStack Compute (Nova)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

__init__.py 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. # Copyright 2010 United States Government as represented by the
  2. # Administrator of the National Aeronautics and Space Administration.
  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. WSGI middleware for OpenStack API controllers.
  18. """
  19. import nova.monkey_patch # noqa
  20. from oslo_log import log as logging
  21. import routes
  22. import webob.dec
  23. import webob.exc
  24. from nova.api.openstack import wsgi
  25. from nova.api import wsgi as base_wsgi
  26. import nova.conf
  27. from nova.i18n import translate
  28. LOG = logging.getLogger(__name__)
  29. CONF = nova.conf.CONF
  30. def walk_class_hierarchy(clazz, encountered=None):
  31. """Walk class hierarchy, yielding most derived classes first."""
  32. if not encountered:
  33. encountered = []
  34. for subclass in clazz.__subclasses__():
  35. if subclass not in encountered:
  36. encountered.append(subclass)
  37. # drill down to leaves first
  38. for subsubclass in walk_class_hierarchy(subclass, encountered):
  39. yield subsubclass
  40. yield subclass
  41. class FaultWrapper(base_wsgi.Middleware):
  42. """Calls down the middleware stack, making exceptions into faults."""
  43. _status_to_type = {}
  44. @staticmethod
  45. def status_to_type(status):
  46. if not FaultWrapper._status_to_type:
  47. for clazz in walk_class_hierarchy(webob.exc.HTTPError):
  48. FaultWrapper._status_to_type[clazz.code] = clazz
  49. return FaultWrapper._status_to_type.get(
  50. status, webob.exc.HTTPInternalServerError)()
  51. def _error(self, inner, req):
  52. LOG.exception("Caught error: %s", inner)
  53. safe = getattr(inner, 'safe', False)
  54. headers = getattr(inner, 'headers', None)
  55. status = getattr(inner, 'code', 500)
  56. if status is None:
  57. status = 500
  58. msg_dict = dict(url=req.url, status=status)
  59. LOG.info("%(url)s returned with HTTP %(status)d", msg_dict)
  60. outer = self.status_to_type(status)
  61. if headers:
  62. outer.headers = headers
  63. # NOTE(johannes): We leave the explanation empty here on
  64. # purpose. It could possibly have sensitive information
  65. # that should not be returned back to the user. See
  66. # bugs 868360 and 874472
  67. # NOTE(eglynn): However, it would be over-conservative and
  68. # inconsistent with the EC2 API to hide every exception,
  69. # including those that are safe to expose, see bug 1021373
  70. if safe:
  71. user_locale = req.best_match_language()
  72. inner_msg = translate(inner.message, user_locale)
  73. outer.explanation = '%s: %s' % (inner.__class__.__name__,
  74. inner_msg)
  75. return wsgi.Fault(outer)
  76. @webob.dec.wsgify(RequestClass=wsgi.Request)
  77. def __call__(self, req):
  78. try:
  79. return req.get_response(self.application)
  80. except Exception as ex:
  81. return self._error(ex, req)
  82. class LegacyV2CompatibleWrapper(base_wsgi.Middleware):
  83. def _filter_request_headers(self, req):
  84. """For keeping same behavior with v2 API, ignores microversions
  85. HTTP headers X-OpenStack-Nova-API-Version and OpenStack-API-Version
  86. in the request.
  87. """
  88. if wsgi.API_VERSION_REQUEST_HEADER in req.headers:
  89. del req.headers[wsgi.API_VERSION_REQUEST_HEADER]
  90. if wsgi.LEGACY_API_VERSION_REQUEST_HEADER in req.headers:
  91. del req.headers[wsgi.LEGACY_API_VERSION_REQUEST_HEADER]
  92. return req
  93. def _filter_response_headers(self, response):
  94. """For keeping same behavior with v2 API, filter out microversions
  95. HTTP header and microversions field in header 'Vary'.
  96. """
  97. if wsgi.API_VERSION_REQUEST_HEADER in response.headers:
  98. del response.headers[wsgi.API_VERSION_REQUEST_HEADER]
  99. if wsgi.LEGACY_API_VERSION_REQUEST_HEADER in response.headers:
  100. del response.headers[wsgi.LEGACY_API_VERSION_REQUEST_HEADER]
  101. if 'Vary' in response.headers:
  102. vary_headers = response.headers['Vary'].split(',')
  103. filtered_vary = []
  104. for vary in vary_headers:
  105. vary = vary.strip()
  106. if (vary == wsgi.API_VERSION_REQUEST_HEADER or
  107. vary == wsgi.LEGACY_API_VERSION_REQUEST_HEADER):
  108. continue
  109. filtered_vary.append(vary)
  110. if filtered_vary:
  111. response.headers['Vary'] = ','.join(filtered_vary)
  112. else:
  113. del response.headers['Vary']
  114. return response
  115. @webob.dec.wsgify(RequestClass=wsgi.Request)
  116. def __call__(self, req):
  117. req.set_legacy_v2()
  118. req = self._filter_request_headers(req)
  119. response = req.get_response(self.application)
  120. return self._filter_response_headers(response)
  121. class APIMapper(routes.Mapper):
  122. def routematch(self, url=None, environ=None):
  123. if url == "":
  124. result = self._match("", environ)
  125. return result[0], result[1]
  126. return routes.Mapper.routematch(self, url, environ)
  127. def connect(self, *args, **kargs):
  128. # NOTE(vish): Default the format part of a route to only accept json
  129. # and xml so it doesn't eat all characters after a '.'
  130. # in the url.
  131. kargs.setdefault('requirements', {})
  132. if not kargs['requirements'].get('format'):
  133. kargs['requirements']['format'] = 'json|xml'
  134. return routes.Mapper.connect(self, *args, **kargs)
  135. class ProjectMapper(APIMapper):
  136. def _get_project_id_token(self):
  137. # NOTE(sdague): project_id parameter is only valid if its hex
  138. # or hex + dashes (note, integers are a subset of this). This
  139. # is required to hand our overlaping routes issues.
  140. project_id_regex = '[0-9a-f-]+'
  141. if CONF.osapi_v21.project_id_regex:
  142. project_id_regex = CONF.osapi_v21.project_id_regex
  143. return '{project_id:%s}' % project_id_regex
  144. def resource(self, member_name, collection_name, **kwargs):
  145. project_id_token = self._get_project_id_token()
  146. if 'parent_resource' not in kwargs:
  147. kwargs['path_prefix'] = '%s/' % project_id_token
  148. else:
  149. parent_resource = kwargs['parent_resource']
  150. p_collection = parent_resource['collection_name']
  151. p_member = parent_resource['member_name']
  152. kwargs['path_prefix'] = '%s/%s/:%s_id' % (
  153. project_id_token,
  154. p_collection,
  155. p_member)
  156. routes.Mapper.resource(
  157. self,
  158. member_name,
  159. collection_name,
  160. **kwargs)
  161. # while we are in transition mode, create additional routes
  162. # for the resource that do not include project_id.
  163. if 'parent_resource' not in kwargs:
  164. del kwargs['path_prefix']
  165. else:
  166. parent_resource = kwargs['parent_resource']
  167. p_collection = parent_resource['collection_name']
  168. p_member = parent_resource['member_name']
  169. kwargs['path_prefix'] = '%s/:%s_id' % (p_collection,
  170. p_member)
  171. routes.Mapper.resource(self, member_name,
  172. collection_name,
  173. **kwargs)
  174. def create_route(self, path, method, controller, action):
  175. project_id_token = self._get_project_id_token()
  176. # while we transition away from project IDs in the API URIs, create
  177. # additional routes that include the project_id
  178. self.connect('/%s%s' % (project_id_token, path),
  179. conditions=dict(method=[method]),
  180. controller=controller,
  181. action=action)
  182. self.connect(path,
  183. conditions=dict(method=[method]),
  184. controller=controller,
  185. action=action)
  186. class PlainMapper(APIMapper):
  187. def resource(self, member_name, collection_name, **kwargs):
  188. if 'parent_resource' in kwargs:
  189. parent_resource = kwargs['parent_resource']
  190. p_collection = parent_resource['collection_name']
  191. p_member = parent_resource['member_name']
  192. kwargs['path_prefix'] = '%s/:%s_id' % (p_collection, p_member)
  193. routes.Mapper.resource(self, member_name,
  194. collection_name,
  195. **kwargs)