OpenStack源码实际上是比较规范的,但是对刚刚接触到源代码的人来说,却感觉有点混乱。我刚开始的时候也常常搞乱,比如service.Service类继承自openstack.common.service.Service类,有个openstack.common.service.Services类,有个openstack.common.service.Launcher类,有个openstack.common.service.ServiceLauncher类,有个openstack.common.service.ProcessLauncher类,nova根目录下有个wsgi.py文件,api/openstack目录下也有个wsgi.py,在总是搞不清这些关系的时候,我也会脱口而出“你妹的”。这篇文章,我将对nova-api涉及的一些关键代码和调用流程以及类关系进行剖析,如果你和我一样是个OpenStack初学者并且也有“你妹的”情节,希望你看过之后,对于OpenStack,少一点“你妹的”的抱怨。
我安装的是Juno版的OpenStack,并通过阅读源代码加调试运行的手段来分析nova-api服务的调用流程。在阅读到某些源码片段,当我理解不了时,我就会在当前源码位置插入Python代码:
import pdb
pdb.set_trace()
然后再运行/usr/bin/nova-api服务。上述代码相当于设置了一个断点,程序运行到该位置会停下来以便我们调试分析。这时候,我们可以单步执行程序,查看调用栈,显示当前源码位置等。
nova源码文件安装到目录“/usr/lib/python2.7/dist-packages/nova”,在之后的分析中,我所列出的目录均是相对于该目录的路径,当我说一个文件路径为api/openstack/wsgi.py时,则该文件存放位置为“/usr/lib/python2.7/dist-packages/nova/api/openstack/wsgi.py”,依此类推。
/usr/bin/nova-api的启动代码如下:
从上面的图中我们知道,它调用了cmd/api.py文件里的main函数,该函数的代码如下图:
我们看到,main函数中有个launcher变量,实际为nova.openstack.common.service.ProcessLauncher对象实例,有个server变量,实际为nova.service.WSGIService对象实例。在我调试的时候,api值为“osapi_compute”,这个值是在/etc/nova/nova.conf文件中配置的。在获取了这样两个对象之后,launcher调用了它的launch_service函数,并将server变量作为参数传递。
我们来看一下WSGIService类的构造函数__init__(),如下图:
在构造函数中,有一个self.app变量,一个self.server变量,后者为wsgi.Server类实例。关于这两个变量,我们后续再做分析。我们先来了解下ProcessLauncher类的launch_service函数:
如上图,launch_service调用了_start_child函数,后者又调用_child_process函数。_child_process代码如下图,其中生成了一个nova.openstack.common.service.Launcher对象,并调用对象的方法launch_service,并传递一个service参数,该service参数实际为在main函数中生成的WSGIService对象,也就是那个刚开始碰到的server变量。
launch_service函数调用了self.services.add(service),从Launcher的__init__初始化函数中我们知道services实际为nova.openstack.common.service.Services对象,那么我们看下这个add函数干了些什么。
从上图我们知道,Services对象中有一个services列表,保存各个WSGIService对象,有一个tg变量为threadgroup.ThreadGroup对象实例,add函数调用tg的add_thread函数,传递一个self.run_service静态方法,一个WSGIService对象和一个Event.event对象。Services.run_service函数调用WSGIService.start函数启动wsgi服务。WSGIService.start函数如下图:
上图中,self.server.start()函数被调用,根据前面的分析,self.server在WSGIService.__init__()函数中被初始化,实际为wsgi.Server类的实例,那么我们再看下wsgi.Server类的start方法,如下图:
这里,我们看到了eventlet.spawn函数被调用,传递的wsgi_kwargs字典中,func为eventlet.wsgi.server,site为self.app。从上面的一系列分析中,我们不难看出,self.app为WSGIService.__init__构造函数中通过self.loader.load_app(name)得到的,其中name值为打印出来的“osapi_compute”。至此,一个作为nova-api服务的WSGI应用服务已经起来了,并且运行在一个greenthread中,它将接收用户的各种请求。
nova-api实现的是wsgi的Application,而不是Server,Server是由wsgi库来实现,就是那个eventlet.wsgi.server,那么问题来了,我们的Application是如何被调用的呢?这时候,你是不是想到了那个eventlet.spawn被调用的时候,字典参数里那个“site”的值了?对了,就是它了,它是在WSGIService.__init__构造函数中通过self.loader.load_app(name)得到的,name为“osapi_compute”,self.loader为wsgi.Loader实例。如下图,load_app调用了deploy.loadapp,传入参数为:“config:/etc/nova/api-paste.ini,name=osapi_compute”。
我们再看下deploy.loadapp都调用了哪些函数,如下图:
可以看到,它调用了api/openstack/urlmap.py模块中的urlmap_factory函数,该函数接收三个参数,分别为loader、global_conf和local_conf。这是怎么做到的呢?且让我们看下api-paste.ini这个文件,如下图:
看到了吗,原来deploy.loadapp根据传入的配置文件路径和name值,找到了api-paste.ini文件中对应的section,顺藤摸瓜找到了urlmap_factory函数。接下来我们以请求路径/v2为例进行阐述,其对应的App为openstack_compute_api_v2,后者又找到了osapi_compute_app_v2,后者又找到了nova.api.openstack.compute:APIRouter.factory,最终调用的是这个函数获取到一个App,而这一系列调用关系的完成是在urlmap_factory函数里边通过loader.get_app()调用来实现的,这里loader的类型为paste.deploy.loadwsgi.ConfigLoader。
我们来看一下nova.api.openstack.compute.APIRouter这个类,并没有看到factory函数,不过它继承自nova.api.openstack.APIRouter类,factory函数就在这个基类中。因此,当nova.api.openstack.compute:APIRouter.factory函数被调用时,返回的应该是一个nova.api.openstack.compute.APIRouter类实例。nova.api.openstack.compute.APIRouter类中有一个_setup_routes函数,用来完成路径到App的映射关系,该函数又是在其基类nova.api.openstack.APIRouter类的__init__()函数中被调用。
我们以名称为servers的请求为例,它对应的controller为self.resources[‘servers’],后者又是通过servers.create_resource(ext_mgr)获取,那我们看下这个函数干了些什么:
上图中,create_resource直接返回了一个nova.api.openstack.wsgi.Resource类实例。Resource类聚合了一个nova.api.openstack.compute.servers.Controller类对象,该Controller类从nova.api.openstack.wsgi.Controller类继承而来。Resource类本身又继承自nova.wsgi.Application类,代表一个wsgi的App。
下面看下nova.api.openstack.APIRouter类的__init__()函数。
上图中,nova.api.openstack.APIRouter类的__init__()函数中有个mapper变量,为ProjectMapper类实例,该mapper变量在调用_setup_routes时作为参数传递以供子类完成路径映射,又传递给其基类wsgi.Router。好了,我们看下这个基类:
在基类的__init__()函数中,调用了routes.middleware.RoutesMiddleware()函数,这个mapper当作参数传递,同时作为参数的还有wsgi.Router的静态函数_dispatch,返回值赋值给self._router变量。我们还看到,wsgi.Router类有个__call__函数,这个函数在子类中均没有实现,再看它的注释就知道,当wsgi请求到来时,webob会调用到这个函数,这个函数将变量self._router变量返回。接着,_dispatch函数将被调用,这个函数将返回一个对应的App,这个App类型为nova.api.openstack.wsgi.Resource。_dispatch函数被调用时,其局部变量的类型和值如下图所示:
上图所示为请求调用detail方法时,match变量的值,它是一个dict,action值为“detail”,controller值为一个nova.api.openstack.wsgi.Resource对象,还有一个project_id。作为一个wsgi的App,nova.api.openstack.wsgi.Resource类实现了__call__方法,该方法接着就会被调用,如下图:
Resource的__call__函数调用_process_stack函数,后者又调用dispatch函数,dispatch函数调用了被当作参数传进来的函数并返回,从上图中我们可以看到,这个函数就是nova.api.openstack.compute.servers.Controller.detail函数。那么问题又来了,Controller的这个detail方法是怎么被找到的?预知详情,请看如下图:
在Resource类的_process_stack函数中,调用dispatch函数之前,调用了get_method函数,get_method函数又调用了_get_method函数,_get_method中有那么一行:“meth = getattr(self.controller, action)”。这里的self.controller就是Resource类实例中聚合的nova.api.openstack.compute.servers.Controller实例,action就是detail函数,getattr返回的meth就是一个函数,_process_stack再将这个meth作为参数传递给dispatch,在dispatch中真正调用了nova.api.openstack.compute.servers.Controller的detail函数。至此,真相已经大白了。
我们再看下nova.api.openstack.compute.servers.Controller的__init__构造函数。在该构造函数中,有一行代码:self.compute_api = compute.API(),compute.API()通过调用importutils.import_object返回一个类对象,这个类对象的类型为nova.compute.api.API。
nova.compute.api.API类如下图:
nova.compute.api.API类聚合了一个nova.image.api.API对象,一个nova.network.neutronv2.api.API对象,一个nova.volume.cinder.API对象,一个nova.api.openstack.compute.contrib.security_groups.NativeNeutronSecurityGroupAPI对象,一个nova.compute.rpcapi.ComputeAPI对象等。我们可以猜测,nova.api.openstack.compute.servers.Controller类的很多操作将是通过nova.compute.api.API来完成。
接下来,我们以创建虚拟机的请求为例,阐述各个类之间的调用流程。下图中,当创建虚拟机请求到达时,会调用到servers.py中的Controller的create函数,然后会调用到nova.compute.api.API中的create函数,再调用到nova.compute.api.API中_create_instance函数,_create_instance调用nova.compute.api.API的compute_task_api函数,该函数只是给实例变量self._compute_task_api赋值,从图中我们知道该值类型为nova.conductor.ComputeTaskAPI。
nova.conductor.ComputeTaskAPI源码如下图:
上图中,nova.conductor.ComputeTaskAPI的初始化函数__init__中,初始化了一个变量self.conductor_compute_rpcapi,其类型为nova.conductor.rpcapi.ComputeTaskAPI,对应源码如下图:
如下图,在nova.compute.api.API的_create_instance函数中,调用了nova.conductor.ComputeTaskAPI的build_instance函数。
如上图,nova.conductor.ComputeTaskAPI的build_instance函数转而调用nova.conductor.rpcapi.ComputeTaskAPI的build_instance函数,后者再调用cctxt.cast将请求发送到消息队列。
nova-api主要类关系图如下:
以上是个人的粗浅理解,由于细节过多这里只解析关键部分,欢迎各位同仁指正!