2019-05-06

作者: 蓝色思念_7d87 | 来源:发表于2019-05-06 22:24 被阅读0次

    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

        本文链接:https://www.haomeiwen.com/subject/oftjoqtx.html