【vdsm 源代码漫步】vdsm的对外接口 (1)

vdsm

前言

vdsm的代码理论上应该是从vdsmd.py开始的,这里是vdsm服务的代码起始地。考虑到模块的耦合性,将其放到vdsm线程管理这个大话题里面比较合适。vdsm开篇的话题还是从vdsm的接口开始。亦即, 如果将vdsm看成一个黑盒的话, 它是怎样提供这种对外功能的。

此模块在一段时间内不断的演化。所以不同版本的vdsm可能代码差异较大。但是基本架构是不变的。演变的详细情况请参考文献1。

本话题需要的背景知识:rpc调用,请自行百度之。

涉及的文件

本模块涉及的文件包含

  1. protocoldetector.py ( 消息的总入口)
  2. rpc/* (rpc路径下的所有文件,处理协议)
  3. api/* (api路径下的所有文件,处理schema)
  4. clientIF.py (这个文件包含内容较多,此处需要关注协议的注册)
  5. API.py (内部对外接口)
  6. yajsonrpc (基础设施。注意这一块的代码是ovirt私有的,google意义不大,建议浏览一下它的代码)

缘起

我们要回答两个问题

  1. 是谁在调用vdsm的接口? 答案:(1) engine (2)其它主机上的vdsm (3)vdsm-client
  2. 怎样调用vdsm的接口? 答案:(1)通过jsonrpc等协议。

vdsm的接口主要是engine调用。engine显然和vdsm(vdsm直接部署在主机上)不在同一个逻辑环境上,

因而使用jsonrpc调用是很自然的选择。在早期的版本上,也存在xmlrpc调用。因为效率的原因,后续的版本就去掉了。vdsm-client模块主要还是方便调试以及查找问题。不建议在生产环境下直接使用,当然查看功能问题还是不大的。有关vdsm-client的话题请查看本网站其它人的话题。

壹引其纲

为了解决上述的问题,vdsm给出了解决方案。基本架构如下(图片来自参考文献1):

【vdsm 源代码漫步】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到函数的最终处理的流程基本讲完了。

未完待续

  1. 本文主要以jsonrpc为列,没有涉及http(基本流程相似)。后续会补上。
  2. yajsonrpc的内容基本没有涉及。但是这是阅读这一块代码的障碍,后续补上。
  3. 涉及到的线程处理,会在其它章节展开。

思考

  1. 如何为vdsm添加一个接口,并在另一台机器上通过jsonrpc进行调用?

(1)在API.py中添加对应的函数。(2)在*.pickle中添加对应的描述。(需要重新生成pickle文件)

参考文献

  1. https://www.ovirt.org/develop/release-management/features/infra/jsonrpc.html
3 条回复 A 作者 M 管理员 E
  1. 感谢分享!

  2. 你好,请问vdsm中的这些json-rpc接口可以设置允许外部调用吗?不想通过ovirt-engine去调。

欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论