ansible1.1源码初步(一)

ansible1.1源码初步一

  • 欢迎进入ansible的世界
    • 开篇说明
    • 源码目录结构
    • 搭建源码调试环境
    • ansible工具初使用
    • ping模块追踪
    • copy模块追踪
    • 小结

欢迎进入ansible的世界

  这里,我将通过调试ansible1.1源码,学习ansible的工作流程。作为菜鸟的学习笔记,也希望这篇文章对也同为初学者的你能有所帮助。

开篇说明

  为什么要学ansible1.1的源码,为什么不直接学习ansible最新的源码?因为我一开始看ansible最新的代码时,发现内容实在是太多了,看的头都大了,特别对我这样的菜鸟来说。后面偶然一天发现了ansible的releases地址, 这个页面上放了ansible的所有发布的地址,从1.1版本到最新的2.9版本代码都在上面。可以看到最小的ansible1.1版本代码只有294k, 而最新的ansible2.9版本代码已经达到了13M,当然非核心代码永远不是大头。通过学习ansible1.1版本的源码,我发现其实工具最最核心的过程没有变化,从一个简单的版本了解复杂工具的原貌,何乐而不为呢?
  对于众多ansible版本,我的学习计划如下:
  1、完全分析ansible1.1版本源码
  2、分析比较ansible1的各版本之间修复的bug以及新增的功能
  3、完整分析ansible1.9.6版本代码
  4、分析ansible2版本代码之间的代码递进过程
  5、主要分析并掌握ansible2.8.5版本源码

源码目录结构

  • ansible-1.1
    • bin
      提供的命令文件,有4个文件,重点学习ansible/ansible-playbook两个
    • lib
      • ansible
        核心源码目录
    • library
      模块目录

来看看ansible的核心源码目录:

  • lib/ansible
    • callback_plugins
      • __init__.py
      • noop.py
    • inventory
      • 解析conf和yml配置文件,后面详细讲解
    • playbook
      • __init__.py
      • play.py
      • task.py
    • runner
      • 核心处理过程,后续详细讲解
    • utils
      • __init__.py
      • module_docs.py
      • plugins.py
      • tenplate.py
    • __init__.py
    • callbacks.py
    • color.py
    • constants.py
    • error.py
    • module_common.py

来看看核心的runner目录,这些是最核心的代码

  • runner
    • action_plugins (模块动作插件)
      • normal.py
    • connection_plugins(paramiko、ssh)
      • paramiko_ssh.py
      • ssh.py
    • filter_plugins (过滤插件)
    • lookup_plugins
    • __init__.py (最关键的代码都在这里,非常重要)
    • connection.py
    • poller.py
    • return_data.py

  整体来看,ansible-1.1版本的代码是比较少的,容易阅读和理解,因此比较适合初步学习,大概认真点调试,3-4天就对ansible-1.1的代码有个初步的认识;大概1~2个星期,坚持阅读和调试,应该就能掌握ansible-1.1的核心内容了。我们也可以从ansible-1.1的代码中收获许多python相关的知识,不过这个版本主要使用python2版本,并不支持python3。

搭建源码调试环境

  搭建调试环境是比较简单的,特别针对python的工程。使用pyenv命令构建虚拟环境,不过pycharm自带虚拟环境,直接使用pycharm自带的虚拟环境,注意使用python2.7即可。具体步骤如下:
  1、sudo python setup.py install
  2、配置pycharm的[Run]->[Edit Configurations],注意Python interpreter要选择我们虚拟环境的解释器,因为我们的源码是在这个环境下的;同时,在这个弹框中我们也可以配置我们要执行的命令,参考我的配置,以及使用源码执行的结果:ansible1.1源码初步(一)_第1张图片ansible1.1源码初步(一)_第2张图片
  从执行结果上看,ansible-1.1版本的结果和最新的ansible-2.8.5结果是类似的,输出的展示还是六年前的样子。深入代码分析,你会发现,代码的核心架构思想也是大致一样的,这说明一个优秀的思想是多么重要啊。

ansible工具初使用

  我们主要使用ansible-1.1的两个命令,后面分析2.8版本源码时,也是这两个:ansible和ansible-playbook。对于ansible1.1的命令使用,现在给出四个模块的使用例子:

  • ping 模块
    • 连通性测试
    • ansible ceph1 -i hosts -m ping
  • shell 模块
    • 远程执行shell命令
    • ansible ceph1 -i hosts -m shell -a “hostname”
  • copy 模块
    • 拷贝本地文件到远端主机
    • ansible ceph1 -i hosts -m copy -a “src=upload.txt dest=/home/store/upload.txt”
  • file 模块
    • 远端主机新建一个目录
    • ansible ceph1 -i hosts -m file -a “path=/home/store state=directory”

  这四个模块属于常用模块,在最新的ansible-2.8.5中也存在,而且功能更加丰富,完善。我们将仔细分析这两个模块(ping和copy)的执行过程。
ansible1.1源码初步(一)_第3张图片

ping模块追踪

  这里会比较仔细跟踪ping模块代码,会比较粗略,如果想仔细了解每个函数的细节,可以参考后续的文章,我会对非常重要的函数进行详细说明,同时也会写一些代码测试函数的输入和输出,方便理解。

  • ansible
if __name__ == '__main__':
    cli = Cli()
    # options和args分别为参数选项
    (options, args) = cli.parse()
    try:
        # 核心函数,就着一个run方法,开启多进程,执行模块任务,得到结果
        (runner, results) = cli.run(options, args)
        # 处理返回结果
        for result in results['contacted'].values():
            if 'failed' in result or result.get('rc', 0) != 0:
                sys.exit(2)
        if results['dark']:
            sys.exit(2)
    except errors.AnsibleError, e:
        # Generic handler for ansible specific errors
        print "ERROR: %s" % str(e)
        sys.exit(1)

继续来看Cli类,以及对应的run方法

class Cli(object):
    def __init__(self):
        # 统计返回结果的类实例,用于统计
        self.stats = callbacks.AggregateStats()
        # 回调类实例
        self.callbacks = callbacks.CliRunnerCallbacks()   
         
    def parse(self):  
        # 解析参数
        ...
        
        # 目标主机参数只能写一个,可以是正则表达
        if len(args) == 0 or len(args) > 1:
            parser.print_help()
            sys.exit(1)
        # options中返回的是解析模块命令和参数的类,args中是目标主机
        return (options, args)  
     
    def run(self, options, args):
        # 目标机器
        pattern = args[0]

        # 解析inventory, 也就是hosts文件中的主机以及变量
        inventory_manager = inventory.Inventory(options.inventory)
        
        ...
        
        # get target hosts
        hosts = inventory_manager.list_hosts(pattern)
        if len(hosts) == 0:
            print >>sys.stderr, "No hosts matched"
            sys.exit(1)
        
        # only list hosts
        if options.listhosts:
            for host in hosts:
                print '    %s' % host
            sys.exit(0)

        # 如果是command模块,需要带参数,不然就会报错
        if options.module_name == 'command' and not options.module_args:
            print >>sys.stderr, "No argument passed to command module"
            sys.exit(1)
            
        # 和sudo相关处理
        ...
     
        # 最核心的类实例
        runner = Runner(
            module_name=options.module_name, module_path=options.module_path,
            module_args=options.module_args,
            remote_user=options.remote_user, remote_pass=sshpass,
            inventory=inventory_manager, timeout=options.timeout,
            private_key_file=options.private_key_file,
            forks=options.forks,
            pattern=pattern,
            callbacks=self.callbacks, sudo=options.sudo,
            sudo_pass=sudopass,sudo_user=options.sudo_user,
            transport=options.connection, subset=options.subset,
            check=options.check,
            diff=options.check
        )

        if options.seconds:
            # 后台执行任务
            print "background launch...\n\n"
            results, poller = runner.run_async(options.seconds)
            results = self.poll_while_needed(poller, options)
        else:
            # 一般没有-p参数,就是前台执行,等待结果返回
            results = runner.run()

        return (runner, results)

   从这里可以看出,最核心的处理就是Runner类中的run方法。继续追踪Runner类中的run方法,Runner类位于文件lib/ansible/runner/__init__.py中,这里也是整个ansible1.1代码运行的核心代码文件。

class Runner(object):
    # 其余核心代码,先忽略
    ...
    
    def run(self):
        # 获取要执行的目标机器
        hosts = self.inventory.list_hosts(self.pattern)
        if len(hosts) == 0:
            # 没有匹配的主机,返回告警信息
            self.callbacks.on_no_hosts()
            return dict(contacted={
   }, dark={
   })

        # 使用多进程
        global multiprocessing_runner
        # multiprocessing_runner注意,就是这个实例
        multiprocessing_runner = self
        # 返回结果
        results = None
        
        # 获取插件函数 ,这个比较重要,在ping模块下,此处p为None
        p = utils.plugins.action_loader.get(self.module_name, self)
        if p and getattr(p, 'BYPASS_HOST_LOOP', None):
            self.host_set = hosts
            result_data = self._executor(hosts[0]).result
            results = [ ReturnData(host=h, result=result_data, comm_ok=True) \
                           for h in hosts ]
            del self.host_set
        elif self.forks > 1:
            # 如果多个主机,就会启动多个进程
            try:
                # 核心的多进程处理函数
                results = self._parallel_exec(hosts)
            except IOError, ie:
                print ie.errno
                if ie.errno == 32:
                    

你可能感兴趣的:(python项目源码分析)