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格式输出

你可能感兴趣的:(2019-05-06)