nova-api公布api服务没实用到一个些框架,基本都是从头写的。在不了解它时,以为它很复杂,难以掌握。花了两三天的时间把它分析一遍后,发现它本身的结构比較简单,主要难点在于对它所使用的一些类库不了解,如paste.deploy/webob/routes。对于paste.deploy,结合它的官网文档把它的源代码看了两遍。webob看的是源代码。routes看的是文档。对于这些类库提供的函数,假设从文档中去理解他们想要做什么,真不是件easy的事。查看事实上现源代码,就明了了。只是在分析源代码过程中,碰到每个类库都去翻一遍它的源代码,这也是很累人的,后期甚至都不想再看下去了,由于脑子比較厌烦了。所以在学习routes时主要是看它的文档,基本理解了。
paste.deploy
用来解析/etc/nova/api-paste.ini文件,载入用于服务的wsgi app。它的功能有:
- api-paste.ini中配置多个wsgi app,deploy可依据传入的app name载入指定的wsgi app;
deploy.loadapp("config:/etc/nova/api-paste.ini", name="osapi-compute")载入api-paste.ini中,名为osapi-compute的WSGI APP,并作为结果返回。
- 通过写入api-paste.ini的配置,可方便地实现特定字符開始的url到特定wsgi app的映射。如:
[composite:osapi_compute] use = call:nova.api.openstack.urlmap:urlmap_factory /: oscomputeversions /v2: openstack_compute_api_v2
通过该配置,以“/v2”開始的url将交给名为openstack_compute_api_v2的WSGI APP处理,其他以“/”开的url就交给oscomputerversions处理。事实上这并不是deploy的功能,而是上面配置的urlmap实现的。只是通过配置文件,再由deploy调用urlmap,使用就更简单了。
- middle ware的简单载入和去除。
[composite:openstack_compute_api_v2] use = call:nova.api.auth:pipeline_factory keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
上面的faultwrap sizelimit authtoken keystonecontext ratelimit都为middle ware(在nova代码中称为MiddleWare,deploy中称为filter),osapi_compute_app_v2才是wsgi app。请求先交给middle ware处理,再由middle ware决定要不要或者怎么交给wsgi app处理。这些middle ware是能够加入和去除的,假设不想对osapi_compute_app_v2进行限速,那么去掉ratelimit就能够了。事实上这是pipeline_factory实现的功能,只是通过deploy来配置载入更方便。
nova-api中提供了非常多服务API:ec2(与EC2兼容的API),osapi_compute(openstack compute自己风格的API),osapi_volume(openstack volume服务API),metadata(metadata 服务API)等。通过配置文件api-paste.ini,能够方便管理这些API。
deploy提供了server、filter、app的概念,当中filter(即middle ware)、app在nova-api被重度使用,的确太好用了。公布每一个API时,它们有时须要一些同样的功能,如:keystone验证、限速、错误处理等功能。nova将这些功能实现为middle ware,假设须要,通过api-paste.ini配置,给它加上就能够。比方,我须要记录每一个訪问nova-api的ip,及它们的訪问次数和訪问的时间。那么我实现一个middle ware--nova.api.middleware:MonitorMiddleware来记录这些数据。通过以下的api-paste.ini配置,就可对nova-api的訪问进行记录了。
[composite:openstack_compute_api_v2] use = call:nova.api.auth:pipeline_factory keystone = faultwrap sizelimit authtoken keystonecontext ratelimit <strong>monitor</strong> osapi_compute_app_v2 [filter:<strong>monitor</strong>] paste.filter_factory = nova.api.middleware:MonitorMiddleware.factory
webob
用于对wsgi app进行封装,简化wsgi app的定义与编写。webob主要提供三种功能。
- Request。该类用于对传给wsgi app的env參数进行封装,简化对HTTP请求參数的訪问和设置。这样的简化体如今(如果用env对Request实例化了一个对象req):
1) 使用间接明了的属性名对HTTP參数进行訪问,req.method可获取HTTP请求方法(替代REQUEST_METHOD);req.scheme可获取HTTP请求协议http or https(替代wsgi.url_scheme);req.body获取请求body(替代wsgi.input)。
2)大量使用property,省去繁琐细节,简化HTTP參数的訪问和设置。req.body直接訪问HTTP请求的body,而不用考虑(body的长度和字符编码);req.POST以字典形式返回POST请求的參数;req.GET以字典形式返回GET请求的參数。
nova.api.openstack.wsgi.Request继承该类,并加了一个缓存已訪问db的记录的功能,和对content_type推断和检測的功能。
- Response。该类用于对HTTP的返回值进行封装。与Request对象类似,相同使用了property简化对http參数的訪问和设置。支持wsgi app一次返回status和body,这样更直观。事实上Response实例本身也是一个wsgi app。
- decorator--wsgify,装饰wsgi app,使其能够以例如以下方式定义:
@webob.dec.wsgify def wsgi_app(req): #do something with req return req.Response(...)当中參数req是一个Request(默认)或其子类(通过wsgify(RequestClass=XXX)指定)的实例,是用env初始化的。req.Response默觉得webob.Response。以该种方式定义的wsgi app,其结果能够以三种形式返回:
1)返回一个字符串。wsgify将其作为body,并加上一些默认的參数,如status=“200 OK", content_type, content_length等,构造成一个HTTP响应结果并返回;
2)返回一个Response实例,直接返回该resp代表的HTTP请求结果;
3)返回一个wsgi app,wsgify会继续调用该app,并返回app的响应结果。
nova.wsgi.Router就是用第三种返回形式,两次返回wsgi app,终于将HTTP请求依据url映射到相应的controller处理。
routes
用来给服务内部定义url到详细函数的映射。deploy也有url到服务映射功能,但这个映射层次偏高一点。依据上面配置,deploy将以“/v2”開始的url将交给名为openstack_compute_api_v2处理。但openstack_compute_api_v2怎么将/v2/project_id/servers/的GET请求交给nova.api.openstack.compute.servers.Controller.index()处理,而且将POST请求交给create()处理呢;怎么将/v2/project_id/servers/id的GET请求交给show()处理呢?这个就是routes.mappers所提供的功能,它依据path和请求方法,将请求映射到详细的函数上。如在nova中,加入/v2/project_id/servers/{list_vm_state, os_vmsum}两个GET请求来分别获取指定VM的状态和VM的总数。可在nova.api.openstack.compute.APIRouter中加入例如以下两行,将请求分别交给list_vm_state和os_vmsum两个函数处理并返回结果:
self.resources['servers'] = servers.create_resource(ext_mgr) mapper.resource("server", "servers", controller=self.resources['servers'], <strong>collection={'list_vm_state': 'GET', 'os_vmsum': 'GET'}</strong>)这里利用了routes.mapper支持restful api的特性,仅用两条指令,就定义了十多个url到函数的映射。当然你能够例如以下方式加入接口,只是代码稍多,风格不那么统一:
mapper.connect("server", "/{project_id}/servers/list_vm_state", controller=self.resources['servers'], action='list_vm_state', conditions={'list_vm_state': 'GET'}) mapper.connect("server", "/{project_id}/servers/os_vmsum", controller=self.resources['servers'], action='os_vmsum', conditions={'os_vmsum': 'GET'})
主题--nova-api服务流程分析
上面介绍了nova-api公布所用到的一些lib库,有了上面的基础知识,再来分析nova-api的公布流量,就比較轻松了。
nova-api能够提供多种api服务:ec2, osapi_compute, osapi_volume, metadata。能够通过配置项enabled_apis来设置启动哪些服务,默认情况下,四种服务都是启动的。
从nova-api的可运行脚本中,能够看出每一个nova-api服务都是通过nova.service.WSGIService来管理的:
class WSGIService(object): def __init__(self, name, loader=None): self.name = name self.manager = self._get_manager() self.loader = loader or wsgi.Loader() self.app = self.loader.load_app(name) self.host = getattr(FLAGS, '%s_listen' % name, "0.0.0.0") self.port = getattr(FLAGS, '%s_listen_port' % name, 0) self.workers = getattr(FLAGS, '%s_workers' % name, None) self.server = wsgi.Server(name, #这里通过eventlet来启动服务 self.app, host=self.host, port=self.port) def start(self): if self.manager: self.manager.init_host() self.server.start() ......
从上可知,WSGIService使用self.app = self.loader.load_app(name)来载入wsgi app,app载入完毕后,使用nova.wsgi.Server来公布服务。Server首先用指定ip和port实例化一个监听socket,并使用wsgi.server以协程的方式来公布socket,并将监听到的http请求交给app处理。对于Server的启动过程,代码上理解还是比較简单的,没多少分析的。以下我们主要来分析处理HTTP请求的wsgi app是怎样构建的,对于每个请求,它是怎样依据url和请求方法将请求分发到详细的详细函数处理的。
上个语句self.loader.load_app(name)中的loader是nova.wsgi.Loader的实例。Loader.load_app(name)运行以下指令,使用deploy来载入wsgi app:
deploy.loadapp("config:%s" % self.config_path, name=name)
self.config_path为api-paste.ini文件路径,一般为/etc/nova/api-paste.ini。name为ec2, osapi_compute, osapi_volume, metadata之中的一个,依据指定的name不同来载入不同的wsgi app。以下以name=“osapi_compute”时,载入提供openstack compute API服务的wsgi app作为详细分析。osapi_compute的配置例如以下
[composite:osapi_compute] use = call:nova.api.openstack.urlmap:urlmap_factory /: oscomputeversions /v2: openstack_compute_api_v2
osapi_compute是调用urlmap_factory函数返回的一个nova.api.openstack.urlmap.URLMap实例,nova.api.openstack.urlmap.URLMap继承paste.urlmap.URLMap,它提供了wsgi调用接口,所以该实例为wsgi app。可是函数nova.api.openstack.urlmap:urlmap_factory与paste.urlmap.urlmap_factory定义全然一样,只是因为它们所在的module不同,使得它们所用的URLMap分别为与它处于同一module的URLMap。paste.urlmap.urlmap_factory咋不支持一个传參,来指定URLMap呢?这样nova就不用重写一样的urlmap_factory了。paste.urlmap.URLMap实现的功能非常easy:依据配置将url映射到特定wsgi app,并依据url的长短作一个优先级排序,url较长的将优先进行匹配。所以/v2将先于/进行匹配。URLMap在调用下层的wsgi app前,会更新SCRIPT_NAME和PATH_INFO。nova.api.openstack.urlmap.URLMap继承了paste.urlmap.URLMap,并写了一堆代码,事实上仅仅是为了实现对请求类型的推断,并设置environ['nova.best_content_type']:假设url的后缀名为json(如/xxxx.json),那么environ['nova.best_content_type']=“application/json”。假设url没有后缀名,那么将通过HTTP headers的content_type字段中mimetype推断。否则默认environ['nova.best_content_type']=“application/json”。
经上面配置载入的osapi_compute为一个URLMap实例,wsgi server的接受的HTTP请求将直接交给该实例处理。它将url为'/v2/.*'的请求将交给openstack_compute_api_v2,url为'/'的请求交给oscomputerversions处理(它直接返回系统版本)。其他的url请求,则返回NotFound。以下继续分析openstack_compute_api_v2,其配置例如以下:
[composite:openstack_compute_api_v2] use = call:nova.api.auth:pipeline_factory noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2 keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2 keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2
openstack_compute_api_v2是调用nova.api.auth.pipeline_factory()返回的wsgi app。pipeline_factory()依据配置项auth_strategy来载入不同的filter和终于的osapi_compute_app_v2。filter的大概配置例如以下:
[filter:faultwrap] paste.filter_factory = nova.api.openstack:FaultWrapper.factoryfilter在nova中相应的是nova.wsgi.Middleware,它的定义例如以下:
class Middleware(Application): @classmethod def factory(cls, global_config, **local_config): def _factory(app): return cls(app, **local_config) return _factory def __init__(self, application): self.application = application def process_request(self, req): return None def process_response(self, response): return response @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): response = self.process_request(req) if response: return response response = req.get_response(self.application) return self.process_response(response)Middleware初始化接收一个wsgi app,在调用wsgi app之前,运行process_request()对请求进行预处理,推断请求是否交给传入的wsgi app,还是直接返回,或者改动些req再给传入的wsgi app处理。wsgi app返回的response再交给process_response()处理。比如,对于进行验证的逻辑,能够放在process_request中,假设验证通过则继续交给app处理,否则返回“Authentication required”。只是查看nova全部Mddlerware的编写,似乎都不用这样的定义好的结构,而是把处理逻辑都放到__call__中,这样导致__call__变得复杂,代码不够整洁。对于FaultWrapper尚可理解,毕竟须要捕获wsgi app处理异常嘛,但其他的Middleware就不应该了。这可能是不同程序猿写,规范就忽略了。
当auth_strategy=“keystone”时,openstack_compute_api_v2=FaultWrapper(RequestBodySizeLimiter(auth_token(NovaKeystoneContext(RateLimitingMiddleware(osapi_compute_app_v2)))))。所以HTTP请求须要经过五个Middleware的处理,才干到达osapi_compute_app_v2。这五个Middleware分别完毕:
1)异常捕获,防止服务内部处理异常导致wsgi server挂掉;
2)限制HTTP请求body大小,对于太大的body,将直接返回BadRequest;
3)对请求keystone对header中token id进行验证;
4)利用headers初始化一个nova.context.RequestContext实例,并赋给req.environ['nova.context'];
5)限制用户的訪问速度。
当HTTP请经过上面五个Middlerware处理后,终于交给osapi_compute_app_v2,它是怎么继续处理呢?它的配置例如以下:
[app:osapi_compute_app_v2] paste.app_factory = nova.api.openstack.compute:APIRouter.factoryosapi_compute_app_v2是调用nova.api.openstack.compute.APIRouter.factory()返回的一个APIRouter实例。nova.api.openstack.compute.APIRouter继承nova.api.openstack.APIRouter,nova.api.openstack.APIRouter又继承nova.wsgi.APIRouter。APIRouter通过A它的成员变量mapper来建立和维护url与controller之间的映射,该mapper是nova.api.openstack.ProjectMapper的实例,它继承nova.api.openstack.APIMapper(routes.Mapper)。APIMapper将每一个url的format限制为json或xml,对于其他扩展名的url将返回NotFound。ProjectMapper在每一个请求url前面加上一个project_id,这样每一个请求的url都须要带上用户所属的project id,所以一般请求的url为/v2/project_id/resources。nova.api.openstack.compute.APIRouter.setup_routes代码例如以下:
class APIRouter(nova.api.openstack.APIRouter): ExtensionManager = extensions.ExtensionManager def _setup_routes(self, mapper, ext_mgr): self.resources['servers'] = servers.create_resource(ext_mgr) mapper.resource("server", "servers", controller=self.resources['servers']) self.resources['ips'] = ips.create_resource() mapper.resource("ip", "ips", controller=self.resources['ips'], parent_resource=dict(member_name='server', collection_name='servers')) ......APIRouter通过调用routes.Mapper.resource()函数建立RESTFUL API,也能够通过routes.Mapper.connect()来建立url与controller的映射。如上所看到的,servers相关请求的controller设为servers.create_resource(ext_mgr),该函数返回的是一个用nova.api.openstack.compute.servers.Controller()作为初始化參数的nova.api.openstack.wsgi.Resource实例,ips相关请求的controller设为由nova.api.openstack.ips.Controller()初始化的nova.api.openstack.wsgi.Resource实例。由于调用mapper.resource建立ips的url映射时,加入了一个parent_resource參数,使得请求ips相关api的url形式为/v2/project_id/servers/server_id/ips。对于limits、flavors、metadata等请求情况类似。当osapi_compute_app_v2接收到HTTP请求时,将调用nova.wsgi.Router.__call__,它的定义例如以下:
class Router(object): def __init__(self, mapper): 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 webob.exc.HTTPNotFound() app = match['controller'] return app这里開始让我迷惑了一下,__call__()怎么能返回一个wsgi app呢,直接返回wsgi app那它又怎么被调用呢?查看一下wsgify源代码,发现假设函数返回的是wsgi app时,它还会被继续调用,并返回它的处理结果。所以它会继续调用self._router,_router是routes.middleware.RoutesMiddleware的实例,使用self._dispatch和self.map来初始化的,self.map是在Router的子类nova.api.openstack.APIMapper.__init__中,被初始化为ProjectMapper实例,并调用_setup_routes建立好url与cotroller之间的映射。routes.middleware.RoutesMiddleware.__call__调用mapper.routematch来获取该url映射的controller等參数,以{"controller":Resource(Controller()), "action": funcname, "project_id": uuid, ...}的格式放在match中。并设置例如以下的environ变量,方便后面调用的self._dispatch訪问。最后调用self._dispatch。
environ['wsgiorg.routing_args'] = ((url), match) environ['routes.route'] = route environ['routes.url'] = url_dispatch详细负责url到controller的映射,它通过前面设置environ['wsgiorg.routing_args']来找到url相应的controller。这里的controller就是通过_setup_resource函数设置的controller,及使用响应Controller初始化的Resource实例。Resource通过environ['wsgiorg.routing_args']获取上面设置的match,该match有一个action属性,它指定了全部调用Crotroller成员函数的名子,以及其他相关的调用參数。在我们定义Controller的成员函数时,一般须要通过nova.api.openstack.wsgi.{serializers, deserializers}来指定解释body内容的模板,能够是xml或者json格式的。前面说过重定义nova.api.openstack.urlmap.URLMap的目的是为了推断content_type。Resource在解析body时会參考content_type,然后调用响应的解析器进行解析(如XMLDeserializer、JSONDeserializer),然后将body update进action_args,使用action_args来调用Controller成员函数,即终于的http请求处理函数。最后将运行结果使用指定的序列化器序列化,并返回结果。
參考文献:
[1] https://docs.python.org/2/library/re.html
[2] http://routes.readthedocs.org/en/latest/restful.html
[3] http://pythonpaste.org/deploy/
[4] https://wiki.python.org/moin/MiniDom