在阅读OpenStack各个组件源码的过程中,发现所有的模块如nova,neutron等,都通过wsgi的方式对外提供restful API。
而且在使用wsgi的过程中,还涉及paste,routes,webob这些python第三方库的使用。因此如果想理解好OpenStack对外提供restful API的代码,必须要先学好python WSGI,以及理解paste,routes,webob这些库所发挥的作用。
在网上也看了许多人写的这方面的相关文章,发现大部分是零星介绍其中某一方面概念的,很少有全面介绍清楚的,因此决定写一篇这种文档分享出来,方便大家阅读理解OpenStack的源码。
所有的这些都是围绕着WSGI的,因此我们先了解第一个概念WSGI。
wsgi是在PEP333中定义的:
PEP:
333 | |
Title: | Python Web Server Gateway Interface v1.0 |
Author: | Phillip J. Eby <pje at telecommunity.com> |
Discussions-To: | Python Web-SIG < web-sig at python.org > |
Status: | Final |
Type: | Informational |
Created: | 07-Dec-2003 |
Post-History: | 07-Dec-2003, 08-Aug-2004, 20-Aug-2004, 27-Aug-2004, 27-Sep-2010 |
Superseded-By: | 3333 |
出现的原因和目标:
python有大量的web框架实现,如Zope,Quixote, Webware, SkunkWeb, PSO, 和 Twisted Web等等。这么多的框架对于python初学者是个困惑,因为他们认为他们选择的web框架会限制它们使用哪个web服务器,反之亦然。
作为一个对比,Java也有很多可用的web框架,但是Java的"servlet API"使使用不同web框架编写的web应用能够运行在不同的支持"servelet API"的web服务器上。
wsgi的目的就是提供一个可以在Python web服务中可以广泛使用的API--------------这样无论这些服务器是使用Python(如Medusa),还是嵌入式Python(如mod_python),或者使用CGI,FastCGI这些网关协议来使用python应用,都可以自由地组合选择python web框架或者python web服务器。这样开源的web服务器和框架开发者可以专注于它们领域的专业化。
因此PEP333的目标就是提供一个在web服务器和web应用程序之间简单且通用的接口:也就是Python Web ServerGateway Interface (WSGI)。
但是WSGI规范对于已经存在的Python web服务器和web框架并没有作用,服务器和框架的作者和维护者需要实现WSGI才能使规范起作用。
然而,由于PEP333提出时并没有web服务器和框架支持WSGI,而且对于实现WSGI并没有直接的奖励,因此WSGI的设计必须容易实现,这样在实现WSGI时的投资和代价可以很低。
这样对于web server和web框架侧接口的实现都应该简单,这对WSGI来说至关重要,这也是任何决策设计的关键原则。
然而需要注意的是,简化框架作者的实现和框架对于web应用容易使用不是一回事.WSGI设计的接口不会有过多的干涉对于框架的作者,比如干涉web响应对象和cookie的控制只会阻碍现有的框架。因此,WSGI只是为了现存的web server和web框架能够方便的交互,而不是发明一个新的web框架。
同样地,WSGI的另一个目标是在任何Python版本中都能够部署。这样,新的标准模块不建议使用,WSGI也不应该要求2.2.2以上的Python版本。
另外,为了简化现有和将来web框架和服务的实现,WSGI应该容易创建request预处理和response后续处理,其它基于WSGI的组件对于包含它们的服务器来说就像一个应用一样,同时对于web服务包含的应用来说看起来就像是一个server。
如果中间件即简单又健壮,并且WSGI广泛地应用在web server和框架中,这就可能产生一个全新的Python web应用框架:由WSGI组件构成的松耦合的框架。另外,现在的框架作者甚至会选择用这种方式来重构他们的框架,这样框架就像是一个使用WSGI的库,而不是一个庞大的框架。这也允许应用开发者为他们的应用选择最合适的组件,而不是只能使用一个既有优点又有缺点的单一框架。
最后,需要提及的一点是,这个PEP333并不涉及部署。在有大量的server和框架实现了WSGI之后,可能会有新的PEP来描述如何部署WSGI服务器和应用框架。
规范概述
WSGI接口包含两方面:"server"或"gateway"侧,和应用和框架侧。服务器侧调用一个应用侧提供的可调用对象。服务器侧规定如何可调用对象如何提供。这里可以假定一些服务器或者网关要求应用的部署都提供一个脚本来创建一个服务或者网关实例,然后给这个实例提供一个应用对象。其它的服务器或者网关则可能使用配置文件或者其它机制来指定应用对象如何导入或者如何获取。
另外,为了使servers/gateways和applications/frameworks保持简洁,也可以创建实现WSGI两侧规范的中间件。这个中间件对包含它们的服务器就像是应用,对于应用来说就像是包含它们的服务器,中间件可以用来提供扩展API,内容转换,导航和其它有用的功能。
在整个规范的描述中,我们将会使用"a callable"来表示一个函数,方法,类或者一个包含__call__方法的实例。选择何种callable来实现取决server,gateway或者应用程序的需要。反过来,server,gateway,或者应用不能依赖于提供给它的callable是如何实现的。callables仅仅是被调用而已。
应用对象是一个简单的callable对象,并接受2个参数。这个对象术语不能被误解为需要一个实际的对象实例:一个函数,方法,类或者拥有__call__方法的实例都可以被当作一个应用对象。应用对象必须能够被调用多次,事实上所有的servers/gateways(除了CGI)都会作出这样的重复要求。
注意:尽管我们称其为“应用”对象,它不应该被理解为:意味着应用开发者使用WSGI作为编程API。它假定应用开发者将会使用现在的,高层次的框架来开发他们的应用。WSGI是一个框架和server开发者工具,它并不直接关心应用开发者。
下面是2个应用对象的例子,一个是一个函数,另外是一个类。
def simple_app(environ, start_response): """Simplest possible application object""" status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return ['Hello world!\n'] class AppClass: """Produce the same output, but using a class (Note: 'AppClass' is the "application" here, so calling it returns an instance of 'AppClass', which is then the iterable return value of the "application callable" as required by the spec. If we wanted to use *instances* of 'AppClass' as application objects instead, we would have to implement a '__call__' method, which would be invoked to execute the application, and we would need to create an instance for use by the server or gateway. """ def __init__(self, environ, start_response): self.environ = environ self.start = start_response def __iter__(self): status = '200 OK' response_headers = [('Content-type', 'text/plain')] self.start(status, response_headers) yield "Hello world!\n"
每当从Http客户端获取到了request请求,Server或者Gateway就调用应用callable。为了描述清楚,下面实现了一个简单的CGI网关,它用方法作为应用对象。注意,这只是一个简单的例子,只有有限的错误处理,因为默认情况下,一个没有捕获的异常将会直接输出到sys.stderr并被web服务器记录到日志。
import os, sys def run_with_cgi(application): environ = dict(os.environ.items()) environ['wsgi.input'] = sys.stdin environ['wsgi.errors'] = sys.stderr environ['wsgi.version'] = (1, 0) environ['wsgi.multithread'] = False environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = True if environ.get('HTTPS', 'off') in ('on', '1'): environ['wsgi.url_scheme'] = 'https' else: environ['wsgi.url_scheme'] = 'http' headers_set = [] headers_sent = [] def write(data): if not headers_set: raise AssertionError("write() before start_response()") elif not headers_sent: # Before the first output, send the stored headers status, response_headers = headers_sent[:] = headers_set sys.stdout.write('Status: %s\r\n' % status) for header in response_headers: sys.stdout.write('%s: %s\r\n' % header) sys.stdout.write('\r\n') sys.stdout.write(data) sys.stdout.flush() def start_response(status, response_headers, exc_info=None): if exc_info: try: if headers_sent: # Re-raise original exception if headers sent raise exc_info[0], exc_info[1], exc_info[2] finally: exc_info = None # avoid dangling circular ref elif headers_set: raise AssertionError("Headers already set!") headers_set[:] = [status, response_headers] return write result = application(environ, start_response) try: for data in result: if data: # don't send headers until body appears write(data) if not headers_sent: write('') # send headers now if body was empty finally: if hasattr(result, 'close'): result.close()
注意,对于一些应用程序,这个单一的对象可能伴演服务器的角色,而对另外一些服务器可能伴演应用的角色。这种中间件组件可以提供下面一些功能:
一般来说,中间件的使用对于"server/gateway"和"application/framework"来说是透明的,并且不需要特殊的支持。希望将中间件并入应用程序的用户只需要向server提供一个组件;对于一个应用,并配置中间件来执行应用,就好像这个中间件组件是server一样。当然,这个中间件包装的应用可能另一个中间件包装的另外一个应用。
大多数情况下,中间件必须同时符合WSGI养病地"server/gateway"和"application/framework"两方面的限制和约束。在一些情况下,相对于单纯的server或application,中间件的要求更加严格,关于这些要求后面将会详细讲述。
下面是一个中间件的例子,它通过使用piglatin.py将text/plain的response转化为"pig Latin"。(注意:一个真正的中间件可能会用更加健壮的方式检查内容的类型和内容的编码,另外,这个简单的例子忽略了单词可能穿越块体边界的情况)。
from piglatin import piglatin class LatinIter: """Transform iterated output to piglatin, if it's okay to do so Note that the "okayness" can change until the application yields its first non-empty string, so 'transform_ok' has to be a mutable truth value. """ def __init__(self, result, transform_ok): if hasattr(result, 'close'): self.close = result.close self._next = iter(result).next self.transform_ok = transform_ok def __iter__(self): return self def next(self): if self.transform_ok: return piglatin(self._next()) else: return self._next() class Latinator: # by default, don't transform output transform = False def __init__(self, application): self.application = application def __call__(self, environ, start_response): transform_ok = [] def start_latin(status, response_headers, exc_info=None): # Reset ok flag, in case this is a repeat call del transform_ok[:] for name, value in response_headers: if name.lower() == 'content-type' and value == 'text/plain': transform_ok.append(True) # Strip content-length if present, else it'll be wrong response_headers = [(name, value) for name, value in response_headers if name.lower() != 'content-length' ] break write = start_response(status, response_headers, exc_info) if transform_ok: def write_latin(data): write(piglatin(data)) return write_latin else: return write return LatinIter(self.application(environ, start_latin), transform_ok) # Run foo_app under a Latinator's control, using the example CGI gateway from foo_app import foo_app run_with_cgi(Latinator(foo_app))
PASTE
是一个方便我们使用WSGI的工具包。它提供了一系列的WSGI中间件,可以嵌套使用它们来构建web应用程序。因此,它属于上面讲述的WSGI的中间件部分。它提供的所有中间件都符合上面的PEP333接口,并同其它基于PEP333的中间件相兼容。
它提供了以下一些特性:
测试方面:
调度:
Web应用:
工具:
调试过滤器:
其它工具:
提到paste,就不得不提PasteDeploy,可以把PasteDeploy看作paste的一个扩展包,它主要是用来发现和配置WSGI应用。对WSGI的使用者,可以方便地从配置文件汇总加载WSGI应用;对于WSGI的开发者,只需要给自己的应用提供一套简单的入口点即可。由于paste提供的中间件符合PEP333规范,因此我们在使用PasteDeploy加载WSGI应用时可以配置使用paste的组件作为WSGI应用,后面讲解openstack应用时会看到其使用paste.urlmap中间件。
下文,用paste泛指paste和PasteDeploy,方便讲解paste的功能和作用。
我们来看看openstack如何使用paste来加载自己的WSGI应用,以neutron作为例子。
首先,我们来看一下neutron api的配置文件,它符合PasteDeploy的定义格式:
/etc/neutron/api-paste.ini:
[composite:neutron]
use = egg:Paste#urlmap
/: neutronversions
/v2.0: neutronapi_v2_0
[composite:neutronapi_v2_0]
use = call:neutron.auth:pipeline_factory
noauth = cors request_id catch_errors extensions neutronapiapp_v2_0
keystone = cors request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:catch_errors]
paste.filter_factory = oslo_middleware:CatchErrors.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = neutron
[filter:keystonecontext]
paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:extensions]
paste.filter_factory = neutron.api.extensions:plugin_aware_extension_middleware_factory
[app:neutronversions]
paste.app_factory = neutron.api.versions:Versions.factory
[app:neutronapiapp_v2_0]
paste.app_factory = neutron.api.v2.router:APIRouter.factory
我们来边解释上面的配置文件边理解paste的应用。首先配置文件和ini文件类似,由一个一个配置段section构成,每个section的格式如下:
[type:name]
其中,type包括以下几种:
配置文件开始是一个[composite:neutron] section,表示这是一个组合类型的配置,它的名字是neutron。组合类型表明它由若干WSGI应用构成。我们来看section中的内容,形式如key = value。
use = egg:Paste#urlmap
/: neutronversions
/v2.0: neutronapi_v2_0
首先,使用key 'use'表明它使用了Paste egg包中paste.urlmap这个中间件的功能,这个中间件的功能就是把根据不同URL前缀将请求路由给不同的WSGI应用。下面的配置表明:
把对"/"的访问路由给neutronversion这个app来处理,把"/v2.0"的访问路由给neutronapi_v2_0这个app来处理。
其中use可以使用以下几种形式:
顺着这个,我们来看下neutronversions这个section的配置:
[app:neutronversions]
paste.app_factory = neutron.api.versions:Versions.factory
这里有个key是"paste.app_factory"这个指明了后面是一个工厂函数,指明了加载的模块和方法。
作为验证,我们来看下具体WSGI应用的实现代码:
neutron/api/versions.py:
class Versions(object): @classmethod def factory(cls, global_config, **local_config): return cls(app=None)可以看到确实是通过factory工厂方法来构造对应的WSGI应用对象。这样对于"/"URL的访问就会交于Version的factory方法构造的callable应用对象。进一步看,这个对象有一个__call__方法,就是在对发送请求时调用的方法:
@webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): """Respond to a request for all Neutron API versions.""" version_objs = [ { "id": "v2.0", "status": "CURRENT", }, ]这个方法用到了@webob.dec.wsgify这个装饰器,它的作用后面介绍webob时详细讲解。
然后再看另外一个neutronapi_v2_0 section的配置:
[composite:neutronapi_v2_0]
use = call:neutron.auth:pipeline_factory
noauth = cors request_id catch_errors extensions neutronapiapp_v2_0
keystone = cors request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
可以看到它是一个composite组合类型的section,它用来构造"/v2.0"URL访问的应用,首先看第一个配置:
use = call:neutron.auth:pipeline_factory
从value "call:neutron.auth:pipeline_factory"看,它是一个call类型的值,说明它使用一个可调用的对象(通常是一个函数)来构造对应的WSGI对象。
neutron/auth.py:
def pipeline_factory(loader, global_conf, **local_conf): """Create a paste pipeline based on the 'auth_strategy' config option.""" pipeline = local_conf[cfg.CONF.auth_strategy] pipeline = pipeline.split() filters = [loader.get_filter(n) for n in pipeline[:-1]] app = loader.get_app(pipeline[-1]) filters.reverse() for filter in filters: app = filter(app) return app可以看到这个方法首先会读取配置中的授权策略,有2种”noauth"和"keystone",然后根据不同的授权策略从配置文件中读取对应的filters和app,这是一个列表。由于默认为"keystone"授权,因此对应的filters就是" cors request_id catch_errors authtoken keystonecontext extensions ",而app就是neutronapiapp_v2_0",这样对于"/v2.0"前缀URL的请求,就会对neutronapiapp_v2_0这个app应用所有的filter处理后返回。然后可以在配置文件中看到所有filter的app的配置:
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:catch_errors]
paste.filter_factory = oslo_middleware:CatchErrors.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = neutron
[filter:keystonecontext]
paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory
[filter:authtoken]
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:extensions]
paste.filter_factory = neutron.api.extensions:plugin_aware_extension_middleware_factory
[app:neutronversions]
paste.app_factory = neutron.api.versions:Versions.factory
[app:neutronapiapp_v2_0]
paste.app_factory = neutron.api.v2.router:APIRouter.factory
这些filter和app中的"key=value"的key有的是paste.app_factory,有的是pase.filter_factory,这些不同key的含义如下:
filter factory与app factory非常相似,只是返回filter而不是WSGI app。返回的filter必须是可调用的,接收WSGI app为唯一的参数,返回处理过的该app。
可以通过下面的代码来确认,可以看到其返回了一个filter,是一个可调用的函数,且接受app为参数,并返回一个app。
neutron/api/extensions.py:
def plugin_aware_extension_middleware_factory(global_config, **local_config): """Paste factory.""" def _factory(app): ext_mgr = PluginAwareExtensionManager.get_instance() return ExtensionMiddleware(app, ext_mgr=ext_mgr) return _factory
这是最常见的factory,接收配置参数,用来返回一个WSGI应用,全局配置以字典的形式传入,局部配置则以关键字参数(keyword arguments)的形式传入。
通过代码来确认下:
neutron/api/versions.py:
class Versions(object): @classmethod def factory(cls, global_config, **local_config): return cls(app=None)
这样就分析完了opensatck是如何通过paste和paste.deploy来加载自己的WSGI应用的。更多关于paste的使用方法,可以参考之前的文章:
http://blog.csdn.net/happyanger6/article/details/54564802
通过paste的分析可知,大部分的restful API请求形如"/v2.0"为前缀,最终都会交于APIRouter的工厂方法构造的对象,那么这个工厂方法又如何将不同的具体请求映射给不同的应用来处理呢?这就是Routes这个库的作用。
Routes可以看作为Ruby rails的路由系统的python实现,它将不同的URL请求映射给相应的应用处理,相反地,也用来为不同的应用构造URL。Routes可以使我们很容易地构造简单干净的RESTFul风格的URLs。
Routes提供基于域名,HTTP方法,cookies,或者函数的条件匹配,对于子域名的支持是内置的。它还有广泛单元测试套件。
下面是一个使用Routes的例子:
# Setup a mapper from routes import Mapper map = Mapper() map.connect(None, "/error/{action}/{id}", controller="error") map.connect("home", "/", controller="main", action="index") # Match a URL, returns a dict or None if no match result = map.match('/error/myapp/4') # result == {'controller': 'error', 'action': 'myapp', 'id': '4'}
那么Openstack是怎么用的呢,前面讲APIRouter时,知道是通过它的工厂类方法factory来构造对应的WSGI应用对象的。
[app:neutronapiapp_v2_0]
paste.app_factory = neutron.api.v2.router:APIRouter.factory
class APIRouter(base_wsgi.Router): @classmethod def factory(cls, global_config, **local_config): return cls(**local_config)而工厂方法只是简单的构造了一个对象,因此我们分析__init__方法来看下这个对象是如何构造的:
def __init__(self, **local_config): mapper = routes_mapper.Mapper() plugin = manager.NeutronManager.get_plugin() ext_mgr = extensions.PluginAwareExtensionManager.get_instance() ext_mgr.extend_resources("2.0", attributes.RESOURCE_ATTRIBUTE_MAP) col_kwargs = dict(collection_actions=COLLECTION_ACTIONS, member_actions=MEMBER_ACTIONS) def _map_resource(collection, resource, params, parent=None): allow_bulk = cfg.CONF.allow_bulk allow_pagination = cfg.CONF.allow_pagination allow_sorting = cfg.CONF.allow_sorting controller = base.create_resource( collection, resource, plugin, params, allow_bulk=allow_bulk, parent=parent, allow_pagination=allow_pagination, allow_sorting=allow_sorting) path_prefix = None if parent: path_prefix = "/%s/{%s_id}/%s" % (parent['collection_name'], parent['member_name'], collection) mapper_kwargs = dict(controller=controller, requirements=REQUIREMENTS, path_prefix=path_prefix, **col_kwargs) return mapper.collection(collection, resource, **mapper_kwargs) mapper.connect('index', '/', controller=Index(RESOURCES)) for resource in RESOURCES: _map_resource(RESOURCES[resource], resource, attributes.RESOURCE_ATTRIBUTE_MAP.get( RESOURCES[resource], dict())) resource_registry.register_resource_by_name(resource) for resource in SUB_RESOURCES: _map_resource(SUB_RESOURCES[resource]['collection_name'], resource, attributes.RESOURCE_ATTRIBUTE_MAP.get( SUB_RESOURCES[resource]['collection_name'], dict()), SUB_RESOURCES[resource]['parent']) # Certain policy checks require that the extensions are loaded # and the RESOURCE_ATTRIBUTE_MAP populated before they can be # properly initialized. This can only be claimed with certainty # once this point in the code has been reached. In the event # that the policies have been initialized before this point, # calling reset will cause the next policy check to # re-initialize with all of the required data in place. policy.reset() super(APIRouter, self).__init__(mapper)可以看出构造mapper映射主要通过内部方法_map_resource完成,通过遍历RESOURCES和SUB_RESOURCES 来添加routes,对于不同的URL请求,通过
base.create_resource来创建对应的controller。RESOURCES = {'network': 'networks', 'subnet': 'subnets', 'subnetpool': 'subnetpools', 'port': 'ports'}
其中base.create_resouce如下:
neutron/api/v2/base.py:
def create_resource(collection, resource, plugin, params, allow_bulk=False, member_actions=None, parent=None, allow_pagination=False, allow_sorting=False): controller = Controller(plugin, collection, resource, params, allow_bulk, member_actions=member_actions, parent=parent, allow_pagination=allow_pagination, allow_sorting=allow_sorting) return wsgi_resource.Resource(controller, FAULT_MAP)可以看到,动态地创建出了处理不同URL所需的Controller。
另外注意到,创建时有member_actions与collections_actions,这些就决定了对应API所支持的操作。
COLLECTION_ACTIONS = ['index', 'create'] MEMBER_ACTIONS = ['show', 'update', 'delete']
最后是webob,它提供了一系列的装饰器来将我们的函数包装成WSGI应用。OpenStack使用它来简化WSGI应用的开发。
还是以最简单的”/"URL处理为例:
class Versions(object): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): """Respond to a request for all Neutron API versions.""" version_objs = [ { "id": "v2.0", "status": "CURRENT", }, ]可以看到使用了@webob.dec.wsgify装饰器,它的作用就是将函数封装成符合WSGI规范的应用,这样调用对应的__call__方法时,就像是如下调用:
app_iter = obj(environ,start_response).注意:obj是一个Versions对象。
参考文档:
PEP333:https://www.python.org/dev/peps/pep-0333/#rationale-and-goals