前言
vdsm的代码理论上应该是从vdsmd.py开始的,这里是vdsm服务的代码起始地。考虑到模块的耦合性,将其放到vdsm线程管理这个大话题里面比较合适。vdsm开篇的话题还是从vdsm的接口开始。亦即, 如果将vdsm看成一个黑盒的话, 它是怎样提供这种对外功能的。
此模块在一段时间内不断的演化。所以不同版本的vdsm可能代码差异较大。但是基本架构是不变的。演变的详细情况请参考文献1。
本话题需要的背景知识:rpc调用,请自行百度之。
涉及的文件
本模块涉及的文件包含
- protocoldetector.py ( 消息的总入口)
- rpc/* (rpc路径下的所有文件,处理协议)
- api/* (api路径下的所有文件,处理schema)
- clientIF.py (这个文件包含内容较多,此处需要关注协议的注册)
- API.py (内部对外接口)
- yajsonrpc (基础设施。注意这一块的代码是ovirt私有的,google意义不大,建议浏览一下它的代码)
缘起
我们要回答两个问题
- 是谁在调用vdsm的接口? 答案:(1) engine (2)其它主机上的vdsm (3)vdsm-client
- 怎样调用vdsm的接口? 答案:(1)通过jsonrpc等协议。
vdsm的接口主要是engine调用。engine显然和vdsm(vdsm直接部署在主机上)不在同一个逻辑环境上,
因而使用jsonrpc调用是很自然的选择。在早期的版本上,也存在xmlrpc调用。因为效率的原因,后续的版本就去掉了。vdsm-client模块主要还是方便调试以及查找问题。不建议在生产环境下直接使用,当然查看功能问题还是不大的。有关vdsm-client的话题请查看本网站其它人的话题。
壹引其纲
为了解决上述的问题,vdsm给出了解决方案。基本架构如下(图片来自参考文献1):
万目皆张
- 前端
首先,vdsm会统一监听54321端口,针对不同的协议(http或者jsonrpc),将消息发给不同的协议栈。这一点是在protocoldetector.py实现的。在clientIF.py/class clientIF(object) 中有代码:
host = config.get(‘addresses’, ‘management_ip’)
port = config.getint(‘addresses’, ‘management_port’)
# When IPv6 is not enabled, fallback to listen on IPv4 address
try:
self._createAcceptor(host, port)
此处会创建一个Acceptor统一接收不同的协议消息。监听端口,以及地址,是在配置文件里面读取的,可以随时修改。那么监听协议是怎么注册到Acceptor中的呢?请阅读如下三个函数。
self._prepareHttpServer()
self._prepareJSONRPCServer()
self._connectToBroker()
前2个函数分别创建了2个server,并通过add_detector 函数,将server对应的detector注册到了Acceptor 里面((self._acceptor.add_detector(http_detector) ))。 上述提到的detector注册,仅仅是将detector加入到类中的_handlers列表中(参看add_detector函数)。我们回到Acceptor里面(即protocoldetector.py)。
可以看到在类MultiProtocolAcceptor的初始化函数里面,创建了一个监听程序(监听54321),并且处于了listen状态(self._acceptor.listen(5))。并且经历一阵复杂的调用注册过程,会调到文件中的如下代码(handle_read函数):
for detector in self._detectors:
if detector.detect(data):
……
detector.handle_socket(sock, (host, port))
break
可以看到,detector的两个关键函数的使用情况。我们接着可以看他们是怎么定义的:在stomp中
def detect(self, data):
return data.startswith(stomp.COMMANDS)
def handle_socket(self, client_socket, socket_address):
self.json_binding.add_socket(self._reactor, client_socket)
在http中:
def detect(self, data):
return data.startswith(“PUT /”) or data.startswith(“GET /”)
def handle_socket(self, client_socket, socket_address):
self.server.add_socket(client_socket, socket_address)
至此完成了注册挂载,并且完成了运行时与客户端的连接。
我们反过头继续看clientIF.py。 在clientIF类里面,除了上述提到的三个注册函数。含有一个函数需要关注
def start(self):
for binding in self.servers.values():
binding.start()
self.thread = concurrent.thread(self._reactor.process_requests, name=’Reactor thread’)
self.thread.start()
可以看到,注册完毕之后,start函数,会启动初始化过程中创建的server。并且启动reactor。我们接着看server
究竟会做什么。找到BindingJsonRpc类。查看start 函数。继续查找,在yajsonrpc/__init__.py里面可以找到函数
serve_requests。其工作内容是获取工作队列的内容,并解析。换句话说,就是处理我们得到的消息。
从函数栈 _parseMessage–>_runRequest–> _serveRequest–>_handle_request 我们找到关键代码:
method = self._bridge.dispatch(req.method)—–代码前后内容忽略
res = method(**params)—–代码前后内容忽略
第一步在bridage里面找到req需要的真实的method。然后调用对应的method。
- 后端
我们跳转到 rpc/Bridage.py 。查看代码是如何根据json的信息,找到对应的跳转函数的。我们找到核心函数
_dynamicMethod。在此之前我们需要了解一下api/*下的函数,主要是schema处理. 不影响主流程阅读,此处不描述。在 _get_api_instance函数中可以看到。函数会根据json提供的类信息通过API.py,生成对应的具体类。
随后进入一个分支
if fn:
result = fn(api, argobj) ——–若是本地有替代方案,不使用API.py中的调用(override)。
else:
fn = getattr(api, methodName) —-根据json的函数名,获得具体的函数(实例化)
正常都会调用第二个分支。 随后就有result = fn(*methodArgs)调用。
至于第一个分支,请关注command_info这个数据结构。简单讲,通过这个数据结构将json信息,直接转换成了被调用函数。
在这里简单补充一下api/* 是如何运作的。schema通过pickle读取*.pickle文件的内容,获取接口的类型以及接口函数参数的信息。从而供Bridge构造执行函数时候使用。
至此。一个从jsonrpc到函数的最终处理的流程基本讲完了。
未完待续
- 本文主要以jsonrpc为列,没有涉及http(基本流程相似)。后续会补上。
- yajsonrpc的内容基本没有涉及。但是这是阅读这一块代码的障碍,后续补上。
- 涉及到的线程处理,会在其它章节展开。
思考
- 如何为vdsm添加一个接口,并在另一台机器上通过jsonrpc进行调用?
(1)在API.py中添加对应的函数。(2)在*.pickle中添加对应的描述。(需要重新生成pickle文件)
参考文献
- https://www.ovirt.org/develop/release-management/features/infra/jsonrpc.html
感谢分享!
你好,请问vdsm中的这些json-rpc接口可以设置允许外部调用吗?不想通过ovirt-engine去调。
可以