Ansible社区目前非常活跃,从1.x到2.x,以及2.x以后的版本都有一些变化,Ansible官方并不支持Python API,不保证API向后兼容。2.0版本重写了大部分Python API,
官网上说2.0后使用Ansible API有些复杂了。
由于最开始没有重视版本间的差异,本地git clone了最新的dev分支代码,按照dev分支的实现用Python调用Ansible API,结果放到运行环境上就报找不到import的module。最后发现运行环境上的是2.3.2.0版本的Ansible,API跟最新的dev分支有很大不同,所以只好把本地代码切换到2.3 stable版本。下面的代码实现了用Python调用Ansible在本机执行playbook的功能,但仅支持2.3版本,代码参考ansible/cli/playbook.py,实际上就是从命令行的执行逻辑中抽取出来的。
!/usr/bin/env python
import os
import sys
from collections import namedtuple
from ansible.parsing.dataloader import DataLoader
from ansible.vars import VariableManager
from ansible.inventory import Inventory
from ansible.utils.vars import load_extra_vars
from ansible.utils.vars import load_options_vars
from ansible.executor.playbook_executor import PlaybookExecutor
Options = namedtuple('Options', ['listtags', 'listtasks', 'listhosts', 'syntax', 'connection','module_path', 'forks', 'remote_user', 'private_key_file', 'ssh_common_args', 'ssh_extra_args', 'sftp_extra_args', 'scp_extra_args', 'become', 'become_method', 'become_user', 'verbosity', 'check', 'extra_vars'])
options = Options(listtags=False, listtasks=False, listhosts=False, syntax=False, connection='local', module_path=None, forks=1, remote_user='', private_key_file=None, ssh_common_args='', ssh_extra_args='', sftp_extra_args='', scp_extra_args='', become=True, become_method='sudo', become_user='root', verbosity=3, check=False, extra_vars={})
loader = DataLoader()
# create the variable manager, which will be shared throughout
# the code, ensuring a consistent view of global variables
variable_manager = VariableManager()
variable_manager.extra_vars = load_extra_vars(loader=loader, options=options)
variable_manager.options_vars = load_options_vars(options)
# create the inventory, and filter it based on the subset specified (if any)
inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=['localhost'])
variable_manager.set_inventory(inventory)
playbook_path = './p1.yaml'
if not os.path.exists(playbook_path):
print '[INFO] The playbook does not exist'
sys.exit()
passwords = {'conn_pass': '', 'become_pass': ''}
pbex = PlaybookExecutor(playbooks=[playbook_path], inventory=inventory, variable_manager=variable_manager, loader=loader, options=options, passwords=passwords)
results = pbex.run()
print results
上面的方法使用独立的Python进程执行没有问题,返回的结果跟直接用命令行执行完全一样。但是如果需要运行在gevent环境中,代码可能会hang。原因是Ansible使用了multiprocessing的queue来保存执行结果,ansible一开始执行就初始化结果队列,尝试取结果,但是因为queue为空,且queue()函数默认是block的,所以相当于独占了整个线程,gevent无法切换。下面是问题的复现代码:
import gevent
from multiprocessing import Queue
qq=Queue()
def foo():
print('Running in foo')
gevent.sleep(0)
print('Explicit context switch to foo again')
def bar():
print('Explicit context to bar')
gevent.sleep(0)
print('Implicit context switch back to bar')
def test_queue():
print "getting queue"
v = qq.get()
print "got %s" % v
def run_gevent():
gevent.joinall([
gevent.spawn(foo),
gevent.spawn(test_queue),
gevent.spawn(bar),
])
if __name__ == '__main__':
run_gevent()
一旦test_queue函数获得执行,整个进程就挂死了。
因为上面的方法不能在gevent环境中执行,所以退而求其次,尝试直接调用CLI命令。其实Ansible在执行时根据task不同经常是需要fork process的,一般使用Ansible的场景对执行速度要求不会很高,所以多创建几个进程也无伤大雅。
import subprocess
import os
cmd = "ansible-playbook /opt/ansible/play.yaml"
my_env = os.environ.copy()
my_env["TOKEN"] = _get_token()
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True, env=my_env)
output, error = process.communicate()
print output
上面给新创建的进程增加了一个环境变量TOKEN。
初步学习Ansible,特别是第一种方式调用Python API,可能理解不完全正确,如果哪位看官有解决方案,请不吝赐教,在此谢过。