1. 路由实现
在上一章中, 我们一起过了admin_api的所有流水线的处理, 其中有好几个点是用来添加路由信息的。这一章我们来详细看看路由的具体实现。
路由是MVC架构中非常重要的一个关键节点, 可以说, 如果没有路由,就不可能去实现一个很好的MVC架构, 路由也是整个系统中最先处理的节点。如果想弄清楚Keystone的整体架构, 路由是必须先搞清楚的。
看具体实现之前, 我们先看看路由的具体用法。比如说有如下一条路由:
import routes.middleware
...
class Ec2Extension(wsgi.ExtensionRouter):
def add_routes(self, mapper):
ec2_controller = controllers.Ec2Controller()
# validation
mapper.connect(
'/ec2tokens',
controller=ec2_controller,
action='authenticate',
conditions=dict(method=['POST']))
假设Keystone的服务器为127.0.0.1:35357, 那么如果我们访问如下地址时:
http://127.0.0.1:35357/ec2tokens
经过路由分发, 代码就会跑到类ec2_controller的authenticate方法中。
知道了怎么使用之后, 我们再来看看它的具体实现。我们找到Ec2Extension的路由父类:
class ExtensionRouter(Router):
def __init__(self, application, mapper=None):
if mapper is None:
mapper = routes.Mapper()
self.application = application
self.add_routes(mapper)
mapper.connect('{path_info:.*}', controller=self.application)
super(ExtensionRouter, self).__init__(mapper)
def add_routes(self, mapper):
pass
@classmethod
def factory(cls, global_config, **local_config):
def _factory(app):
conf = global_config.copy()
conf.update(local_config)
return cls(app, **local_config)
return _factory
在ExtensionRouter的__init__ 方法中, routes.Mapper创建了一个Mappper对象。这是一个实际完成路由功能的模块, 这里我们不去管里面的具体实现, 有兴趣的可以参考routes – Route and Mapper core classes。
至此,可以看到, 在ExtensionRouter的factory方法中,创建一个ExtensionRouter的一个实例, 并在其它构造方法中,创建Mapper对象,并把所有的路由信息全部增加到Mapper对象中,然后调用其它父类的构造方法。继续看看Router类的构造方法。
class Router(object):
def __init__(self, mapper):
if CONF.debug:
logging.getLogger('routes.middleware')
self.map = mapper
self._router = routes.middleware.RoutesMiddleware(self._dispatch,self.map)
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, req):
return self._router
@staticmethod
@webob.dec.wsgify(RequestClass=Request)
def _dispatch(req):
match = req.environ['wsgiorg.routing_args'][1]
if not match:
return render_exception(
exception.NotFound(_('The resource could not be found.')),
user_locale=req.best_match_language())
app = match['controller']
return app
在Route类的__init__ 中使用, 使用Router._dispath方法作为一个中间件的应用程序初始化一个中间件。找到这个中间件的实现方法。在routes.middleware.py 中, 其实现如下:
class RoutesMiddleware(object):
self.app = wsgi_app
self.mapper = mapper
self.singleton = singleton
self.use_method_override = use_method_override
self.path_info = path_info
self.log_debug = logging.DEBUG >= log.getEffectiveLevel()
if self.log_debug:
log.debug("Initialized with method overriding = %s, and path "
"info altering = %s", use_method_override, path_info)
def __call__(self, environ, start_response):
"""Resolves the URL in PATH_INFO, and uses wsgi.routing_args
to pass on URL resolver results."""
old_method = None
if self.use_method_override:
req = None
# In some odd cases, there's no query string
try:
qs = environ['QUERY_STRING']
except KeyError:
qs = ''
if '_method' in qs:
req = Request(environ)
req.errors = 'ignore'
if '_method' in req.GET:
old_method = environ['REQUEST_METHOD']
environ['REQUEST_METHOD'] = req.GET['_method'].upper()
if self.log_debug:
log.debug("_method found in QUERY_STRING, altering "
"request method to %s",
environ['REQUEST_METHOD'])
elif environ['REQUEST_METHOD'] == 'POST' and is_form_post(environ):
if req is None:
req = Request(environ)
req.errors = 'ignore'
if '_method' in req.POST:
old_method = environ['REQUEST_METHOD']
environ['REQUEST_METHOD'] = req.POST['_method'].upper()
if self.log_debug:
log.debug("_method found in POST data, altering "
"request method to %s",
environ['REQUEST_METHOD'])
# Run the actual route matching
# -- Assignment of environ to config triggers route matching
if self.singleton:
config = request_config()
config.mapper = self.mapper
config.environ = environ
match = config.mapper_dict
route = config.route
else:
results = self.mapper.routematch(environ=environ)
if results:
match, route = results[0], results[1]
else:
match = route = None
if old_method:
environ['REQUEST_METHOD'] = old_method
if not match:
match = {}
if self.log_debug:
urlinfo = "%s %s" % (environ['REQUEST_METHOD'],
environ['PATH_INFO'])
log.debug("No route matched for %s", urlinfo)
elif self.log_debug:
urlinfo = "%s %s" % (environ['REQUEST_METHOD'],
environ['PATH_INFO'])
log.debug("Matched %s", urlinfo)
log.debug("Route path: '%s', defaults: %s", route.routepath,
route.defaults)
log.debug("Match dict: %s", match)
url = URLGenerator(self.mapper, environ)
environ['wsgiorg.routing_args'] = ((url), match)
environ['routes.route'] = route
environ['routes.url'] = url
if route and route.redirect:
route_name = '_redirect_%s' % id(route)
location = url(route_name, **match)
log.debug("Using redirect route, redirect to '%s' with status"
"code: %s", location, route.redirect_status)
start_response(route.redirect_status,
[('Content-Type', 'text/plain; charset=utf8'),
('Location', location)])
return []
# If the route included a path_info attribute and it should be used to
# alter the environ, we'll pull it out
if self.path_info and 'path_info' in match:
oldpath = environ['PATH_INFO']
newpath = match.get('path_info') or ''
environ['PATH_INFO'] = newpath
if not environ['PATH_INFO'].startswith('/'):
environ['PATH_INFO'] = '/' + environ['PATH_INFO']
environ['SCRIPT_NAME'] += re.sub(
r'^(.*?)/' + re.escape(newpath) + '$', r'\1', oldpath)
response = self.app(environ, start_response)
# Wrapped in try as in rare cases the attribute will be gone already
try:
del self.mapper.environ
except AttributeError:
pass
return response
代码看起来有点多, 我们只要找关键部分及连接部分。首先这是个可调用的类。在其__call__ 中, 调用其app, 并返回其返回值。
但是这样,肯定是不够的, 因为在之前的步骤中, 我们把所有的路由信息全部交给了它来处理, 也就是说在这里,我们需要找到URL请求中,所对应的controller及action. 仔细查找,可以看到如下代码:
if self.singleton:
config = request_config()
config.mapper = self.mapper
config.environ = environ
match = config.mapper_dict
route = config.route
else:
results = self.mapper.routematch(environ=environ)
if results:
match, route = results[0], results[1]
else:
match = route = None
...
url = URLGenerator(self.mapper, environ)
environ['wsgiorg.routing_args'] = ((url), match)
environ['routes.route'] = route
environ['routes.url'] = url
这里有两种方法来查找路由。在Keystone的实现中,是通过routes.middleware.RoutesMiddleware(self._dispatch,self.map)
来实现的, 所以self.singleton的值为默认值True.
那么这里又通过config = request_config()创建了一个新的对象。继续找到其实现, 在routes 的__init__.py中, 代码如下:
def request_config(original=False):
obj = _RequestConfig()
try:
if obj.request_local and original is False:
return getattr(obj, 'request_local')()
except AttributeError:
obj.request_local = False
obj.using_request_local = False
return _RequestConfig()
它创建了一个_RequestConfig对象,在同一个文件中, 找到这个类的实现。
class _RequestConfig(object):
__shared_state = threading.local()
def __getattr__(self, name):
return getattr(self.__shared_state, name)
def __setattr__(self, name, value):
"""
If the name is environ, load the wsgi envion with load_wsgi_environ
and set the environ
"""
if name == 'environ':
self.load_wsgi_environ(value)
return self.__shared_state.__setattr__(name, value)
return self.__shared_state.__setattr__(name, value)
def __delattr__(self, name):
delattr(self.__shared_state, name)
def load_wsgi_environ(self, environ):
"""
Load the protocol/server info from the environ and store it.
Also, match the incoming URL if there's already a mapper, and
store the resulting match dict in mapper_dict.
"""
if 'HTTPS' in environ or environ.get('wsgi.url_scheme') == 'https' \
or environ.get('HTTP_X_FORWARDED_PROTO') == 'https':
self.__shared_state.protocol = 'https'
else:
self.__shared_state.protocol = 'http'
try:
self.mapper.environ = environ
except AttributeError:
pass
# Wrap in try/except as common case is that there is a mapper
# attached to self
try:
if 'PATH_INFO' in environ:
mapper = self.mapper
path = environ['PATH_INFO']
result = mapper.routematch(path)
if result is not None:
self.__shared_state.mapper_dict = result[0]
self.__shared_state.route = result[1]
else:
self.__shared_state.mapper_dict = None
self.__shared_state.route = None
except AttributeError:
pass
if 'HTTP_X_FORWARDED_HOST' in environ:
# Apache will add multiple comma separated values to
# X-Forwarded-Host if there are multiple reverse proxies
self.__shared_state.host = \
environ['HTTP_X_FORWARDED_HOST'].split(', ', 1)[0]
elif 'HTTP_HOST' in environ:
self.__shared_state.host = environ['HTTP_HOST']
else:
self.__shared_state.host = environ['SERVER_NAME']
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
self.__shared_state.host += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
self.__shared_state.host += ':' + environ['SERVER_PORT']
在这个类的实现中, 可以看到其只是定义了一些方法, 但是并没有任何调用。
但是再回到RoutesMiddleware的实现中,这里给config对象设置了两个值。
config = request_config()
config.mapper = self.mapper
config.environ = environ
match = config.mapper_dict
route = config.route
这个时候再回头看看_RequestConfig的实现, 可以看到在其setattr 方法中,如果属性名为environ, 它就会调用load_wsgi_environ, 然后就可找到如下代码:
mapper = self.mapper
path = environ['PATH_INFO']
result = mapper.routematch(path)
if result is not None:
self.__shared_state.mapper_dict = result[0]
self.__shared_state.route = result[1]
else:
self.__shared_state.mapper_dict = None
self.__shared_state.route = None
至此, 可以看到其调用的Mapper对象的routematch方法来查找路由,并将其值放到mapper_dict, route中。最终把它取出来,并放到环境变量中:
match = config.mapper_dict
route = config.route
url = URLGenerator(self.mapper, environ)
environ['wsgiorg.routing_args'] = ((url), match)
environ['routes.route'] = route
environ['routes.url'] = url
现在我们回忆下调用的整体过程:
在服务器收到请求后,交给paste.deployment来处理, 然后paste.deployment调用各个中间件进行处理,直到wsgi.ComposingRouter,
然后调用它。最后会调用Router._dispath方法,在_dispath中,从req.environ[‘wsgiorg.routing_args’][1] 取出match的值,然后取出controller,
app = match[‘controller’]. 这样代码就跑到controller中。
@staticmethod
@webob.dec.wsgify(RequestClass=Request)
def _dispatch(req):
match = req.environ['wsgiorg.routing_args'][1]
if not match:
return render_exception(
exception.NotFound(_('The resource could not be found.')),
user_locale=req.best_match_language())
app = match['controller']
return app
到此为止, 代码可以跑到app中, 也就是Ec2Controller, 也可看到Ec2Controller是Application的一个子类, 在Application中,它实现了一个__call__ 方法,也就是说,它也是一个适用WSGI标准的应用程序。它的__call__ 方法定义如下:
def __call__(self, req):
arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict.pop('action')
del arg_dict['controller']
LOG.debug(_('arg_dict: %s'), arg_dict)
# allow middleware up the stack to provide context, params and headers.
context = req.environ.get(CONTEXT_ENV, {})
context['query_string'] = dict(req.params.iteritems())
context['headers'] = dict(req.headers.iteritems())
context['path'] = req.environ['PATH_INFO']
params = req.environ.get(PARAMS_ENV, {})
for name in ['REMOTE_USER', 'AUTH_TYPE']:
try:
context[name] = req.environ[name]
except KeyError:
try:
del context[name]
except KeyError:
pass
params.update(arg_dict)
context.setdefault('is_admin', False)
# TODO(termie): do some basic normalization on methods
method = getattr(self, action)
# NOTE(vish): make sure we have no unicode keys for py2.6.
params = self._normalize_dict(params)
try:
result = method(context, **params)
except exception.Unauthorized as e:
LOG.warning(
_('Authorization failed. %(exception)s from %(remote_addr)s') %
{'exception': e, 'remote_addr': req.environ['REMOTE_ADDR']})
return render_exception(e, user_locale=req.best_match_language())
except exception.Error as e:
LOG.warning(e)
return render_exception(e, user_locale=req.best_match_language())
except TypeError as e:
LOG.exception(e)
return render_exception(exception.ValidationError(e),
user_locale=req.best_match_language())
except Exception as e:
LOG.exception(e)
return render_exception(exception.UnexpectedError(exception=e),
user_locale=req.best_match_language())
if result is None:
return render_response(status=(204, 'No Content'))
elif isinstance(result, basestring):
return result
elif isinstance(result, webob.Response):
return result
elif isinstance(result, webob.exc.WSGIHTTPException):
return result
response_code = self._get_response_code(req)
return render_response(body=result, status=response_code)
可以看出, 它用action = arg_dict.pop('action')
,从路由信息中,把action给取了出来,然后通过method = getattr(self, action)
把对应的action作为一个可调用的对象取出来,并且调用它。 result = method(context, **params)
到此, 路由已经成功的跑进了我们想要的地方。