这里,我将通过调试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的核心源码目录:
来看看核心的runner目录,这些是最核心的代码
整体来看,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要选择我们虚拟环境的解释器,因为我们的源码是在这个环境下的;同时,在这个弹框中我们也可以配置我们要执行的命令,参考我的配置,以及使用源码执行的结果:
从执行结果上看,ansible-1.1版本的结果和最新的ansible-2.8.5结果是类似的,输出的展示还是六年前的样子。深入代码分析,你会发现,代码的核心架构思想也是大致一样的,这说明一个优秀的思想是多么重要啊。
我们主要使用ansible-1.1的两个命令,后面分析2.8版本源码时,也是这两个:ansible和ansible-playbook。对于ansible1.1的命令使用,现在给出四个模块的使用例子:
这四个模块属于常用模块,在最新的ansible-2.8.5中也存在,而且功能更加丰富,完善。我们将仔细分析这两个模块(ping和copy)的执行过程。
这里会比较仔细跟踪ping模块代码,会比较粗略,如果想仔细了解每个函数的细节,可以参考后续的文章,我会对非常重要的函数进行详细说明,同时也会写一些代码测试函数的输入和输出,方便理解。
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: