【Ansible API】
Ansible本身就是由python写成,所有其对python形式的API的支持应该不错。
其API分不同的版本,这个版本也就是ansible本身的版本,可以通过ansible --version命令查看或者在python中import ansible然后查看anisble.__version__。
在2.0的版本以前,ansible的API十分简单。通过大概十几行代码就可以模拟通过ad-hoc的方式运行annsible命令的。
但是在2.0及以后的版本,难度突然陡增,与此同时更多的更加底层的东西被开放给一般开发,虽然复杂了,但是也更加灵活了。
对于一些简单的需求,可能我们还是喜欢老版API,因此这里有个网上别人封装好的2.0版本API简化文件,通过这里的代码我们可以依然简单地运行ansible的ad-hoc。同时,它的源码支持修改,从而达到更个性化的改造。
*** 需要注意,下面这个代码只在2.0开头的几个版本中适用。至少到2.4.0之后,API又有了一些改动,下面的代码运行会出错。我懒得再研究2.4了,就干脆pip install ansible==2.0来配合这个库
ansible_api.py:
#-*- coding:utf-8 -*-
importosimportsysfrom collections importnamedtuplefrom ansible.parsing.dataloader importDataLoaderfrom ansible.vars importVariableManagerfrom ansible.inventory importInventoryfrom ansible.inventory.group importGroupfrom ansible.inventory.host importHostfrom ansible.playbook.play importPlayfrom ansible.executor.task_queue_manager importTaskQueueManagerfrom ansible.executor.playbook_executor importPlaybookExecutorfrom ansible.plugins.callback importCallbackBaseclassResultsCollector(CallbackBase):def __init__(self, *args, **kwargs):
super(ResultsCollector, self).__init__(*args, **kwargs)
self.host_ok={}
self.host_unreachable={}
self.host_failed={}defv2_runner_on_unreachable(self, result):
self.host_unreachable[result._host.get_name()]=resultdef v2_runner_on_ok(self, result, *args, **kwargs):
self.host_ok[result._host.get_name()]=resultdef v2_runner_on_failed(self, result, *args, **kwargs):
self.host_failed[result._host.get_name()]=resultclassMyInventory(Inventory):"""this is my ansible inventory object."""
def __init__(self, resource, loader, variable_manager):"""resource的数据格式是一个列表字典,比如
{
"group1": {
"hosts": [{"hostname": "10.0.0.0", "port": "22", "username": "test", "password": "pass"}, ...],
"vars": {"var1": value1, "var2": value2, ...}
}
}
如果你只传入1个列表,这默认该列表内的所有主机属于my_group组,比如
[{"hostname": "10.0.0.0", "port": "22", "username": "test", "password": "pass"}, ...]"""self.resource=resource
self.inventory= Inventory(loader=loader, variable_manager=variable_manager, host_list=[])
self.gen_inventory()def my_add_group(self, hosts, groupname, groupvars=None):"""add hosts to a group"""my_group= Group(name=groupname)#if group variables exists, add them to group
ifgroupvars:for key, value ingroupvars.iteritems():
my_group.set_variable(key, value)#add hosts to group
for host inhosts:#set connection variables
hostname = host.get("hostname")
hostip= host.get('ip', hostname)
hostport= host.get("port")
username= host.get("username")
password= host.get("password")
ssh_key= host.get("ssh_key")
my_host= Host(name=hostname, port=hostport)
my_host.set_variable('ansible_ssh_host', hostip)
my_host.set_variable('ansible_ssh_port', hostport)
my_host.set_variable('ansible_ssh_user', username)
my_host.set_variable('ansible_ssh_pass', password)
my_host.set_variable('ansible_ssh_private_key_file', ssh_key)#set other variables
for key, value inhost.iteritems():if key not in ["hostname", "port", "username", "password"]:
my_host.set_variable(key, value)#add to group
my_group.add_host(my_host)
self.inventory.add_group(my_group)defgen_inventory(self):"""add hosts to inventory."""
ifisinstance(self.resource, list):
self.my_add_group(self.resource,'default_group')elifisinstance(self.resource, dict):for groupname, hosts_and_vars inself.resource.iteritems():
self.my_add_group(hosts_and_vars.get("hosts"), groupname, hosts_and_vars.get("vars"))classAnsibleAPI(object):"""This is a General object for parallel execute modules."""
def __init__(self, resource, *args, **kwargs):
self.resource=resource
self.inventory=None
self.variable_manager=None
self.loader=None
self.options=None
self.passwords=None
self.callback=None
self.__initializeData()
self.results_raw={}def __initializeData(self):"""初始化ansible"""Options= namedtuple('Options', ['connection', 'module_path', 'forks', 'timeout', 'remote_user','ask_pass', 'private_key_file', 'ssh_common_args', 'ssh_extra_args','sftp_extra_args','scp_extra_args', 'become', 'become_method', 'become_user', 'ask_value_pass','verbosity','check', 'listhosts', 'listtasks', 'listtags', 'syntax'])#initialize needed objects
self.variable_manager =VariableManager()
self.loader=DataLoader()
self.options= Options(connection='smart', module_path='/usr/share/ansible', forks=100, timeout=10,
remote_user='root', ask_pass=False, private_key_file=None, ssh_common_args=None,
ssh_extra_args=None,
sftp_extra_args=None, scp_extra_args=None, become=None, become_method=None,
become_user='root', ask_value_pass=False, verbosity=None, check=False, listhosts=False,
listtasks=False, listtags=False, syntax=False)
self.passwords= dict(sshpass=None, becomepass=None)
self.inventory=MyInventory(self.resource, self.loader, self.variable_manager).inventory
self.variable_manager.set_inventory(self.inventory)defrun(self, host_list, module_name, module_args):"""run module from andible ad-hoc.
module_name: ansible module_name
module_args: ansible module args"""
#create play with tasks
play_source =dict(
name="Ansible Play",
hosts=host_list,
gather_facts='no',
tasks=[dict(action=dict(module=module_name, args=module_args))]
)
play= Play().load(play_source, variable_manager=self.variable_manager, loader=self.loader)#actually run it
tqm =None
self.callback=ResultsCollector()try:
tqm=TaskQueueManager(
inventory=self.inventory,
variable_manager=self.variable_manager,
loader=self.loader,
options=self.options,
passwords=self.passwords,
)
tqm._stdout_callback=self.callback
tqm.run(play)finally:if tqm is notNone:
tqm.cleanup()defrun_playbook(self, host_list, role_name, role_uuid, temp_param):"""run ansible palybook"""
try:
self.callback=ResultsCollector()
filenames= ['' + '/handlers/ansible/v1_0/sudoers.yml'] #playbook的路径
template_file = '' #模板文件的路径
if notos.path.exists(template_file):
sys.exit()
extra_vars= {} #额外的参数 sudoers.yml以及模板中的参数,它对应ansible-playbook test.yml --extra-vars "host='aa' name='cc' "
host_list_str = ','.join([item for item inhost_list])
extra_vars['host_list'] =host_list_str
extra_vars['username'] =role_name
extra_vars['template_dir'] =template_file
extra_vars['command_list'] = temp_param.get('cmdList')
extra_vars['role_uuid'] = 'role-%s' %role_uuid
self.variable_manager.extra_vars=extra_vars#actually run it
executor =PlaybookExecutor(
playbooks=filenames, inventory=self.inventory, variable_manager=self.variable_manager,
loader=self.loader,
options=self.options, passwords=self.passwords,
)
executor._tqm._stdout_callback=self.callback
executor.run()exceptException as e:print "error:",e.messagedefget_result(self):
self.results_raw= {'success': {}, 'failed': {}, 'unreachable': {}}for host, result inself.callback.host_ok.items():
self.results_raw['success'][host] =result._resultfor host, result inself.callback.host_failed.items():
self.results_raw['failed'][host] = result._result.get('msg') orresult._resultfor host, result inself.callback.host_unreachable.items():
self.results_raw['unreachable'][host] = result._result['msg']return self.results_raw
简单的用法是这样的:
#-*- coding:utf-8 -*-
from ansible_api importAnsibleAPI
resource=[
{'hostname':'localtest','ip','192.168.178.59','username':'root','password':'xxx'},
{'hostname':'localtest2','ip':'192.168.178.141','username':'root','password':'yyy'} #有个小坑,hostname中不能有空格,否则这个host会被ansible无视
]
api=AnsibleAPI(resource)#开始模拟以ad-hoc方式运行ansible命令
api.run(
['localtest','localtest2'], #指出本次运行涉及的主机,在resource中定义
'command', #本次运行使用的模块
'hostname' #模块的参数
)#获取结果,是一个字典格式,如果是print可以用json模块美化一下
importjsonprint json.dumps(api.get_result(),indent=4)
从逻辑上看,首先我们声明了一个resource,在这是一个list结构,其中包含了各个被操作主机的信息。hostname,ip,username,password这些是作为ansible连接所用的最基本的几个参数,此外port也可指定。resource被api类加载,这个过程其实就是生成了一个动态的inventory。从源码中的90多行可以看出,当传入一个list时api默认将其中所有host信息都放入一个default_group的inventory组,当传入一个dict就默认这个dict的各个键值对分别是组名和组中host信息。
run方法是真正的执行方法,其参数从前往后三个分别是host_list, module, module_args。command的话args比较简单。像类似于copy这类模块的参数可以这么写:
api.run(['test'],'copy','src="/tmp/testfille" dest="/tmp/newfile"')
而file就可以这样:
api.run(['test'],'path="/tmp/test.py" mode=0755 owner="tmpuser"')
通过这两个例子基本就可以看清楚如何向run方法传递ansible模块的参数了。
api在执行run方法之后并不会主动输出结果,需要我们手动地get_result()。result在空的情况下是这样一个结构:
{"success": {},"failed": {},"unreachable":{}
}
不难看出,从主机层面上,主机们被分成了执行成功,执行失败,和连接不到三类,分别给出结果。
下面给出一个返回结果的示例
{"failed": {"node-one": {"cmd": ["cat","/tmp/test"],"end": "2018-05-08 16:27:29.327685","_ansible_no_log": false,"stdout": "","changed": true,"failed": true,"delta": "0:00:00.003369","stderr": "cat: /tmp/test: \u6ca1\u6709\u90a3\u4e2a\u6587\u4ef6\u6216\u76ee\u5f55","rc": 1,"invocation": {"module_name": "command","module_args": {"creates": null,"executable": null,"chdir": null,"_raw_params": "cat /tmp/test","removes": null,"warn": true,"_uses_shell": false
}
},"stdout_lines": [],"start": "2018-05-08 16:27:29.324316","warnings": []
}
},"success": {"localtest": {"cmd": ["cat","/tmp/test"],"end": "2018-05-08 16:27:30.692616","_ansible_no_log": false,"stdout": "","changed": true,"start": "2018-05-08 16:27:30.689329","delta": "0:00:00.003287","stderr": "","rc": 0,"invocation": {"module_name": "command","module_args": {"creates": null,"executable": null,"chdir": null,"_raw_params": "cat /tmp/test","removes": null,"warn": true,"_uses_shell": false
}
},"stdout_lines": [],"warnings": []
}
},"unreachable": {"node-two": "ERROR! SSH encountered an unknown error during the connection. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue"}
}
View Code
localtest执行成功,返回中有changed,delta,start/end,stdout等可能要在后续处理中用到的各种数据
node-one执行失败,所以可以读取stderr中的内容。由于返回是中文,在这里以unicode的形式展现
node-two无法连接,只给出了简单的无法连接的提示信息
基本使用就说到这里吧。下面我要使用了,过程中可能会要回过头来改源码。