Ansible 实现原理(源码分析)

这篇文章 gtt 憋了非常久,一直没有时间坐下来好好整理思路,看了下草稿保存时间是2016年2月17日,尼玛都快过了一年了,这可不行,自己挖的坑含泪也要跳,争取在17日之前完工,不能留下人生遗憾。本文分析的是 ansible 1.x 版本的实现原理,2.0 中批量执行的核心逻辑是一样的,可以类推。

使用方法回顾

先回顾下 ansible 的使用场景,从使用场景出发 gtt 将剖析 ansible 里的黑魔法到底是个啥。

批量执行命令: adhoc command

比如:

$ ansible -i hosts all -m shell -a "whoami"  
192.168.1.50 | success | rc=0 >>
ubuntu

在比如:

$ ansible -i hosts all  -u ubuntu -m apt -a "name=git state=installed"
192.168.1.50 | success >> {
    "changed": false
}

执行 playbook

把上面两个命令写成一个 playbook,达到的目的一样:

$ cat play1.yaml

---
- hosts: all
  remote_user: ubuntu
  tasks:
  - name: show user
    shell:
     command: whoami
  - name: install git
    apt:
      name: git
      state: installed

执行:

$ ansible-playbook play1.yaml  -i hosts 

PLAY [all] ******************************************************************** 

GATHERING FACTS *************************************************************** 
ok: [192.168.1.50]

TASK: [show user] ************************************************************* 
changed: [192.168.1.50]

TASK: [install git] *********************************************************** 
ok: [192.168.1.50]

PLAY RECAP ******************************************************************** 
192.168.1.50               : ok=3    changed=1    unreachable=0    failed=0   

远程执行实现原理

现在站在 ansible 的作者的立场,我们的典型实用场景是远程执行命令。那 playbook 怎么弄?playbook 从实现上说,可以执行完一个命令后自动执行下一个命令,外加处理一些逻辑,比如错误处理,重试等。所以先搞定远程执行命令,playbook的执行就水到渠成。

Ansible 最引以为豪的是 agentless 架构,不用在目标主机上安装任何agent,只要能 ssh 连接到目标主机,并且目标主机上已经安装了 python 解释器,Ansible 就能工作。面对这样的需求,Ansible 的作者 Michael DeHaan 是这么想得(其实是 gtt 乱猜的):

根据参数生成一个 python 脚本,接着通过 ssh 此脚本 copy 到目标主机上,再用 python 执行脚本,最后打扫战场走人。

这里的 python 脚本在 Ansible 中叫做 module,即预先定的一些逻辑,比如 apt module,将所有需要修改的地方以参数的形式传入到 module 中,可以通过直接修改 module 源代码或者传入参数的方法搞定。比如 ansible -m apt -a "name=git" 这行命令就可以解析成:将参数 name=git 传入到 apt module中,然后执行 apt module。

具体看 apt module 的代码,源代码位于:https://github.com/ansible/ansible-modules-core/blob/stable-1.9/packaging/os/apt.py

这个文件类似一个脚本文件,入口函数 main() 解析了传入的参数 name=git ,解析完成开始执行业务逻辑。

def main():
    module = AnsibleModule(
        argument_spec = dict(
            state = dict(default='present', choices=['installed', 'latest', 'removed', 'absent', 'present', 'build-dep']),
            ...
        ),
    )
    p = module.params
    ...
    if p['state'] in ('latest', 'present', 'build-dep'):
        ### 拼接出类似 "apt-get install %(pacakges)s" 的命令,然后执行它。
        result = install(module, packages, cache, upgrade=state_upgrade,
                default_release=p['default_release'],
                install_recommends=install_recommends,
                force=force_yes, dpkg_options=dpkg_options,
                build_dep=state_builddep)
        (success, retvals) = result
        if success:
            module.exit_json(**retvals)
        else:
            module.fail_json(**retvals)
    ...

if __name__ == "__main__":
    main()

有了 module 之后,Ansible 就通过 ssh 通道,将文件内容 copy 到目标主机上,存放在用户家目录下(这个路径是可以配置的),比如 gtt 测试时用的是 ubuntu 用户,Ansible 就创建了一个路径为/home/ubuntu/.ansible/tmp/ansible-tmp-1486872622.14-192988328438393 的目录,里面放每一个 task 的 module 源代码,执行每个 module 就是执行一个 python 脚本。当 python 脚本执行完后删除这个目录,不留一丝痕迹,深藏功与名。

Ansible 提供了非常非常多的 modules,比如 shell,file,apt,yum 等,这些 module 的源代码位于:https://github.com/ansible/ansible-modules-core/tree/stable-1.9 ,有兴趣的同学可以自己研究。

批量执行实现原理

上面搞定了一台主机的远程命令执行,Ansible 可是可以控制一大波设备的,社区有人在生产环境中同时控制1000多台的主机。不管到底消息准确不准确,知道实现原理我们都有办法测试。

如何批量执行?多进程,这里注意是进程,不是线程,更不是协程。源代码:https://github.com/ansible/ansible/blob/stable-1.9/lib/ansible/runner/init.py#L1375

如果 forks 设置大于1,那么启动多个进程,将所有 host 放进一个队列中,进程消费队列中的 host。所以理论上有多少 worker 意味着同时有多少 ssh 连接在处理。

def _parallel_exec(self, hosts):
    ''' handles mulitprocessing when more than 1 fork is required '''

    manager = multiprocessing.Manager()
    job_queue = manager.Queue()
    for host in hosts:
        job_queue.put(host)
    result_queue = manager.Queue()

    try:
        fileno = sys.stdin.fileno()
    except ValueError:
        fileno = None

    workers = []
    for i in range(self.forks):
        new_stdin = None
        if fileno is not None:
            try:
                new_stdin = os.fdopen(os.dup(fileno))
            except OSError, e:
                # couldn't dupe stdin, most likely because it's
                # not a valid file descriptor, so we just rely on
                # using the one that was passed in
                pass
        prc = multiprocessing.Process(target=_executor_hook,
            args=(job_queue, result_queue, new_stdin))
        prc.start()
        workers.append(prc)

    try:
        for worker in workers:
            worker.join()
    except KeyboardInterrupt:
        for worker in workers:
            worker.terminate()
            worker.join()

    results = []
    try:
        while not result_queue.empty():
            results.append(result_queue.get(block=False))
    except socket.error:
        raise errors.AnsibleError("<interrupted>")
    return results

因为进程比较重,所以并发数会受到一些限制,不过对于配置管理,本来就没有很强的并发要求,如果真有,可以使用两台 Ansible master 节点,将 hosts 一分为二,这样也算是一种横向扩展方法。

完。Happy Hacking!

发表评论

电子邮件地址不会被公开。 必填项已用*标注