Openstack Keystone 认证流程(五)--路由

来源:互联网 发布:mac系统盘在哪 编辑:程序博客网 时间:2024/04/29 01:25

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.routeelse:    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'] = routeenviron['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.mapperconfig.environ = environmatch = config.mapper_dictroute = config.route

这个时候再回头看看_RequestConfig的实现, 可以看到在其setattr 方法中,如果属性名为environ, 它就会调用load_wsgi_environ, 然后就可找到如下代码:

mapper = self.mapperpath = 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_dictroute = config.routeurl = URLGenerator(self.mapper, environ)environ['wsgiorg.routing_args'] = ((url), match)environ['routes.route'] = routeenviron['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)

到此, 路由已经成功的跑进了我们想要的地方。

0 0
原创粉丝点击