【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 -*- 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.inventory.group import Group from ansible.inventory.host import Host from ansible.playbook.play import Play from ansible.executor.task_queue_manager import TaskQueueManager from ansible.executor.playbook_executor import PlaybookExecutor from ansible.plugins.callback import CallbackBase class ResultsCollector(CallbackBase): def __init__(self, *args, **kwargs): super(ResultsCollector, self).__init__(*args, **kwargs) self.host_ok = {} self.host_unreachable = {} self.host_failed = {} def v2_runner_on_unreachable(self, result): self.host_unreachable[result._host.get_name()] = result def v2_runner_on_ok(self, result, *args, **kwargs): self.host_ok[result._host.get_name()] = result def v2_runner_on_failed(self, result, *args, **kwargs): self.host_failed[result._host.get_name()] = result class MyInventory(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 if groupvars: for key, value in groupvars.iteritems(): my_group.set_variable(key, value) # add hosts to group for host in hosts: # 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 in host.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) def gen_inventory(self): """ add hosts to inventory. """ if isinstance(self.resource, list): self.my_add_group(self.resource, 'default_group') elif isinstance(self.resource, dict): for groupname, hosts_and_vars in self.resource.iteritems(): self.my_add_group(hosts_and_vars.get("hosts"), groupname, hosts_and_vars.get("vars")) class AnsibleAPI(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) def run(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 not None: tqm.cleanup() def run_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 not os.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 in host_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() except Exception as e: print "error:",e.message def get_result(self): self.results_raw = {'success': {}, 'failed': {}, 'unreachable': {}} for host, result in self.callback.host_ok.items(): self.results_raw['success'][host] = result._result for host, result in self.callback.host_failed.items(): self.results_raw['failed'][host] = result._result.get('msg') or result._result for host, result in self.callback.host_unreachable.items(): self.results_raw['unreachable'][host] = result._result['msg'] return self.results_raw
簡單的用法是這樣的:
# -*- coding:utf-8 -*- from ansible_api import AnsibleAPI 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模塊美化一下 import json print 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" } }
localtest執行成功,返回中有changed,delta,start/end,stdout等可能要在后續處理中用到的各種數據
node-one執行失敗,所以可以讀取stderr中的內容。由於返回是中文,在這里以unicode的形式展現
node-two無法連接,只給出了簡單的無法連接的提示信息
基本使用就說到這里吧。下面我要使用了,過程中可能會要回過頭來改源碼。