2019-05-06
Ansible如何进行源码分析?这是一个提问篇
之前一直停留在使用上,现在需要了解源码,内部是如何工作的,因为ansible源码是python写的,我很庆幸,因为我了解python语言,可是最近看了源码,有种要崩溃的赶脚,怎么办
为了跟踪源码,我写了基于ansible-api的ad-hoc代码段,
环境:
Ansible 2.7.3
Python 2.7.5
Pycharm 2018.1
Centos7.0
准备工作:
1、 ad-hoc代码段大致如下网址所示(不是我写的,但是跟我写的非常像)
https://blog.csdn.net/python_tty/article/details/73822071
2、 ansible大致执行流程如下图所示,抄自51cto,了解流程对源码分析会有所帮助
目的:
1、要了解代码的执行流程
2、要了解模块或者插件的调用入口
3、要知道执行结果如何返回的,最好能知道常用模块的工作原理
源码分析原理:
源码的入口在哪,源码段是如何定义的,通过哪些语句调用的,它的返回值有哪些
源码分析:
良心声明:该有的步骤我是写了,但是我不会分析,这里所写的都是我想象的,没有经验之谈,希望有人指点一二
入口:
根据基于ansible-api写的ad-hoc代码段,我定义的入口为
定义:
Play初始化,_attributes初始化,返回给play
初始化Play()类,然后调用类中的load()方法
return返回遍历输入数据结构并分配任何值
以看到_attrbutes中已经存在该方法的定义了
按照以上的加载过程,将我们要执行的模块都加载进来了,执行完成之后,将对象返回给play,因此play也就有了属性以及对应的方法了
Ad-hoc执行入口run(play)
加载所有回调函数到_module_cache字典中(path和对象)
以下作为我的入口程序,原因是基础到数据都准备好了,比如play(将需要的任务,以及对应任务的定义),inventory,variable_manager,options,loader,password,stdout_callback都准备就绪,开始执行接下来的定义以及调用的部分了
有初始化类TaskQueueManager,调用该类的run方法参数为play对象
进入如下的模块,task_queue_manager.py中,执行
/*注释是这样的,
Iterates over the roles/tasks in a play,using the given (or default)
使用给定(或默认)迭代剧中的角色/任务
strategy for queueing tasks.
任务排队策略。
The default is the linear strategy, which
默认是线性策略*/
执行self.load_callbacks(),跳转到同一个模块(task_queue_manager.py)下的如下函数中,
/*注释是这样的:
Loads all available callbacks, with theexception of those which
加载所有可用的回调函数,除了那些
utilize the CALLBACK_TYPE option.
使用CALLBACK_TYPE选项*/
下面进入callback_loader.all(xxx)加载所有可用的回调函数了
在往下是_module_cache自定的形式存储path和对应的对象
至此,将callback插件都加载进来了,如下所示
收集连接信息(连接信息,比如options的信息):
play_context = PlayContext(new_play,self._options, self.passwords, self._connection_lockfile.fileno())
发送回调函数执行如下列表中的两个方法,(具体做什么用的,我也不知道!)
['v2_playbook_on_play_start', 'v2_on_any']
self.send_callback('v2_playbook_on_play_start',new_play)
初始化线程池:
self._initialize_processes(min(self._options.forks,iterator.batch_size))
下一步进入如下的代码段执行
/*给的注释是:
The linear strategy is simple - get thenext task and queue
线性策略很简单——获取下一个任务和队列
it for all hosts, then wait for the queueto drain before
它适用于所有主机,然后等待队列耗尽之前
moving on to the next task
继续下一个任务*/
按照我的理解就是从队列中取出一个个任务执行,直到队列为空,执行结束,是不是这样子的呢,是这样的
首先了解参数是什么?iterator是可迭代的任务,play_context是连接的参数
下面进入代码进行分析吧
可以看到代码是写在了linear.py模块中了,在代码中,你可以看到一行注释
/*iteratate over each task, while there isone left to runiteratate over each task, while there is one left to run
对每个任务进行迭代,只要还有一条路可走
*/这句话感觉有点励志,就是对每个任务进行迭代
hosts_left = self.get_hosts_left(iterator)
这是获取可用的主机名称列表
本次为了方便,我只设置主机为localhost,下一步,获取对应的任务
linear.py模块中执行如下的函数获取下一个任务,会执行以下1,2,3,步骤执行文件linear.py ---》 task.py ---》 base.py,完成的功能,初始化task任务,并得到主机和任务的元祖
1)
2)
3)
初始化数据,loader,variable在对象被加载后将被提供
第一步骤中,下图标记的是获取主机名称和对应的任务清单
任务执行:
以下开始任务的执行阶段了,前面主机名和任务以及变量的信息均已初始化好了(host_tasks{主机名:任务对象},task_vars获取变量的对象),接下来开始执行了
Task_queue_manager.py
Linear.py中执行如下语句,开始进行执行的部分了
Strategy/__init__.py文件中执行如下的函数,参数:主机名,任务,变量,连接参数
/*给的注释是这样的:
handles queueing the task up to be sent toa worker
处理将任务排队发送给工作人员*/
执行如下的语句,跳转对应的函数执行,对此处有点蒙蔽的状态,先看下程序备注的解释
作用是/*create adummy object with plugin loaders set as an easier way to share them with theforked processes
创建一个虚拟对象,并设置插件加载器,以便更容易地与分叉进程共享它们*/
跳转到对应的函数如下:
给出的文档注释:
/*A simple object to make pass the variousplugin loaders to
一个简单的对象,用来传递各种插件加载器
the forked processes over the queue easierAsimple object to make pass the various plugin loaders to
分叉的进程通过队列easierA简单对象传递各种插件加载器*/
好了,假装理解以上的作用,进入下面分析阶段
下面开始任务的执行了
参数: self._final_q 不太清楚是啥东东
Task_vars: 任务需要的变量信息,这部分信息跟多,ansible_connection ansible_playbook_python 。。。
Host: localhost
Task: file
Play_context: ansible的连接信息,比如options一大堆
….
点击进入如下的函数中执行
/*给出的注释是这样的:
The worker thread class, which usesTaskExecutor to run tasks
工作线程类,它使用TaskExecutor运行任务
read from a job queue and pushes resultsinto a results queue
从作业队列读取并将结果推入结果队列
for reading later.
以后阅读。*/ 真正拿数据 ---执行 ----给数据到队列中的过程了
创建一个进程了,下面开始start执行起来
以下是执行的部分,没太看懂,主进程加入了孩子进程,没看到执行,到底怎么回事呢
可以看到在本机的目录中/root/.ansible/tmp/中生成了以下的文件,这和ansible流程中的在本地产生文件,然后将封装的执行语句和对应的模块,通过ssh发送到受控端貌似有点贴近了,生成的文件已经到了受控端,执行完成后就立即删除了,所以受控端没有看到产生的临时文件了
下面再看
Task,file执行完毕,接下来看这样一段代码,这是做什么的?
提醒下,这前面有缩进,当前是在host:localhost task:file的for循环中
/*给出的函数注释是这样的:不知道什么鬼
Closure to wrap ``StrategyBase.
结束以结束“strategy
ybase”。
_process_pending_results`` and invoke thetask debugger
并调用任务调试器*/
执行完毕后返回的结果如下所示,将执行的结果返回给列表result
接着执行下一个task吧!
没想到下一个是这个meta,我的程序中没有meta的任务,以下看出确实没有做什么
接下来的任务应该是我设置的task:shell语句了
进入执行部分
跳转到执行的函数中,进入如下语句的执行,可以看到发生的变化
在本地/root/.ansible/tmp中多出来command文件,也在预料中,同时发送到受控段同级目录中存在.py可执行文件,执行得到执行结果返回给主控端
下面开始另外一个模块的执行了(也是最后一个fetch模块)
同样根据参数获取fetch.py插件的对象
跳转语句执行对应的函数模块
对应的执行函数如下:执行
同样执行完了,下面看下本地在/root/.ansible/tmp/下是否产生对应fetch文件吧
至此三个ad-hoc执行完成,(file,shell,fetch)
胡乱总结以下:
任务的初始化(包括收集属性以及对应的callback插件对象,获取连接信息,以及异常捕获)
将数据分配给任务执行部分,进行调用执行
最终执行在线程中,(这是最重要一点,掌握这点,ansible源码的分析就差不多了)
调用:
出口:
执行结果的取值,一定在以下的调用函数中产生
Linear.py模块中如下的语句
会调用到如下的函数中
是不是这样呢,进行验证以下
进入此函数中,可以看到,task_result已经是第一个task产生的结果数据了
继续往下看:
Task_result._result,是锁定到task的执行结果,结果中如果有diff会执行对应下面的语句快了
实时证明是有的,所以执行语句块
实时证明callback中已经记录了对应的数据信息了
类中已经记录了这些数据,那么我们在继承该类中,同样能取到对应的数据的信息了
例如以下自己写的代码中继承自callbackbase中,而callbackbase类就是在/callback中的__init__.py中,能够利用result取到对应的值的信息。
将结果信息整理成我们想要的格式就可以了
重点知识掌握:
特殊语句的使用
obj = getattr(self._module_cache[path],self.class_name)
issubclass(obj, plugin_class)
args = FieldAttribute(isa='dict',default=dict)
if original_task.loop: 这是什么鬼,loop是系统默认还是自己定义的标记呢
装饰器的使用
这是什么语句,执行完成后还会跳转到这个位置,想装饰器的使用,但是在装饰器外面没有对应的函数定义,那是不是和@functools.wraps(func)有关呢
多线程的使用,需要重点理解掌握
遗留问题:
在给定的任务,如何确定那个模块可以执行这个任务(1)
主控端如何产生临时文件的,临时文件又是如何传递到受控端的(2)
多线程的处理,弄不大清楚(3)
重点知识掌握,需要掌握哦(4)
接下来要掌握的是具体模块的原理(5)
(1)问题解答:
出现在任务执行的过程之前,for循环host,task的时候,建检索task的时候,会根据task的名称来检索对应目录下的模块的文件的完整路径,然后加载进来到制定的字典中,下面看下这个的过程是怎样的
Linear.py文件中执行如下的语句,这个就是获取action的完整动作的
先初始化action_loader,然后调用其中的Get方法,进入如下的函数,我想要根据self.find_plugin函数参数是任务fetch,来找到模块的完整路径,问题来了self.find_plugin如何存储这么多的数据的呢
跟踪到self.find_plugin函数到如下所示:问题进一步针对到了self._find_plugin函数上,这个为什么也有很多很多的模块在里面缓存呢
带着疑问继续跟踪此函数到如下所示:原来这些数据都是根据plugin所在的路径,根据以.py为结尾的文件全部检索出来得到的,至此,能够按照最开是的self.find_plugin来检索数据也就不奇怪了
由以上基本上可以确定,模块是通过任务名称来确定的,也就是说在制定的目录下,模块文件名称和任务名称相等就能够检索到对应的模块加载进来,进行执行操作了
于是就可以通过以下的代码来加载对应的模块文件了
可以看到path已经对应上了任务所对应的模块文件的完整路径了
接下来就就是把完整路径作为key 模块的对象作为value加载到self._module_cache字典中,进入self._load_module_source函数中,如下所示,得到所对应的模块对象
经验总结:
1. 如果需要自定义模块,只需要在制定的位置,按照任务的名称为命名的执行模块就可以了
2. [Callback回显的信息,我们可以根据得到的原始数据进行定制化输出自己的样式,常见的是json格式输出