├── .gitignore ├── README.md ├── __init__.py ├── myinventory.py ├── playbook_runner.py ├── runner.py └── test ├── debug.yml ├── playbook_test.py ├── runner_test.py └── two_play.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.swp 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible2_myAPI 2 | 3 | Ansible1.9版本的API的使用是非常简单的,但是进入2.0后,Ansible开发者对其代码结构 进行了重组,开放出来的API异常'笨重'。 4 | 5 | `Ansible2_myAPI`是仿照Ansible1.9 API的使用风格,对Ansible2.0+ API的简单封装, 使得2.0+ API的使用像1.9 API一样简单。 6 | 7 | [官方Ansible API](http://docs.ansible.com/ansible/dev_guide/developing_api.html#python-api-2-0) 8 | 9 | 10 | ## 特点 11 | * Ansible的Inventory 不限于hosts静态文件和动态生成(可执行文件),支持Python容器对象(列表,字典,字符串),详见下文。 12 | * 支持`Ad-hoc`和`playbook`两种执行方式。 13 | * 支持组变量, 主机变量, 额外变量。 14 | 15 | ## 测试环境 16 | * python2.7测试通过,其他python版本未测试 17 | * Ansible2.0 ~ 2.3 18 | * Ansible2.4 ~ 2.5 19 | * 秘钥免登陆访问 20 | 21 | ## 分支说明 22 | 因ansible2.4版本较2.3版本改动较大,所以开一个分支兼容2.4以上版本 23 | * master分支: 支持ansible2.0 ~ 2.3本版本 24 | * for-2.4分支: 支持ansible2.4 + 2.5 版本 25 | 26 | ## Inventory 扩展 27 | 根据官方动态主机仓库(Dynamic Inventory)的使用规则,扩展了主机仓库的形式。 28 | 官方支持的inventory: 29 | * 主机列表 30 | ```python 31 | hosts = ['1.1.1.1', '2.2.2.2'] 32 | ``` 33 | * 字符串 34 | ```python 35 | hosts = "1.1.1.1,2.2.2.2" # ','分割符两边不能有空格 36 | hosts = "1.1.1.1," # 单个主机也必须有',' 37 | ``` 38 | * 可执行文件 39 | ```python 40 | hosts = "path/to/execalbe_file" # 给定一个可执行的文件 41 | ``` 42 | * 静态hosts文件 43 | ```python 44 | hosts = "/path/to/hosts" # ini格式 45 | ``` 46 | 47 | 扩展的inventory: 48 | * 字符串 49 | ```python 50 | hosts = "1.1.1.1" # 修复必须有','bug. 51 | hosts = "1.1.1.1, 2.2.2.2" # ','两边可以有空格 52 | ``` 53 | * 字典 54 | ```python 55 | hosts = { 56 | "group1": ["1.1.1.1", "2.2.2.2"], #格式1 57 | "group2": { #格式2 58 | "hosts": ["1.1.1.1", "2.2.2.2"], 59 | "vars":{"some_vars": "some_values"}, 60 | "children": ["other_group"], 61 | }, 62 | "3.3.3.3": { # 格式3 63 | "some_var2": "some_value2", 64 | "foo": "bar" 65 | } 66 | "_meta": { # 主机变量 67 | "hostvars": { 68 | "1.1.1.1": {"var1": "value1"}, 69 | "2.2.2.2": {"var2": "value2"}, 70 | "3.3.3.3": {"var3": "value3"} 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## 使用示例: 77 | Ad-hoc 78 | ```python 79 | from pprint import pprint 80 | from Ansible2_myAPI.runner import Runner 81 | 82 | runner = Runner( 83 | module_name="shell", 84 | module_args="uptime", 85 | remote_user="root", 86 | pattern="all", 87 | hosts="192.168.1.100, 1.1.1.1" 88 | ) 89 | 90 | result = runner.run() 91 | 92 | pprint(result) 93 | ``` 94 | 输出 95 | ```python 96 | {'contacted': { 97 | '192.168.1.100': { 98 | '_ansible_no_log': False, 99 | '_ansible_parsed': True, 100 | u'changed': True, 101 | u'cmd': u'uptime', 102 | u'delta': u'0:00:00.006604', 103 | u'end': u'2017-01-16 16:47:44.051826', 104 | 'invocation': { 105 | u'module_args': { 106 | u'_raw_params': u'uptime', 107 | u'_uses_shell': True, 108 | u'chdir': None, 109 | u'creates': None, 110 | u'executable': None, 111 | u'removes': None, 112 | u'warn': True}, 113 | 'module_name': u'command'}, 114 | u'rc': 0, 115 | u'start': u'2017-01-16 16:47:44.045222', 116 | u'stderr': u'', 117 | u'stdout': u' 16:47:44 up 570 days, 1:40, 1 user, load average: 0.01, 0.02, 0.05', 118 | 'stdout_lines': [u' 16:47:44 up 570 days, 1:40, 1 user, load average: 0.01, 0.02, 0.05'], 119 | u'warnings': []}}, 120 | 'dark': {'1.1.1.1': { 121 | 'changed': False, 122 | 'msg': u'Failed to connect to the host via ssh: ssh: connect to host 1.1.1.1 port 22: Connection timed out\r\n', 123 | 'unreachable': True}}} 124 | 125 | ``` 126 | 127 | playbook 128 | ```python 129 | from pprint import pprint 130 | from Ansible2_myAPI.playbook_runner import PlaybookRunner 131 | 132 | runner = PlaybookRunner( 133 | playbook_path="some.yml", 134 | hosts="192.168.1.100, 1.1.1.1", 135 | ) 136 | 137 | result = runner.run() 138 | pprint(result) 139 | ``` 140 | some.yml 141 | ```YAML 142 | --- 143 | - name: Test the plabybook API. 144 | hosts: all 145 | remote_user: root 146 | gather_facts: yes 147 | tasks: 148 | - name: exec uptime 149 | shell: uptime 150 | ``` 151 | 输出 152 | ``` 153 | {'plays': [{'play': {'id': '9627b6a0-4507-4682-a1f4-242c51577b83', 154 | 'name': u'Test the plabybook API.'}, 155 | 'tasks': [{'hosts': { 156 | '1.1.1.1': {'changed': False, 157 | 'msg': u'Failed to connect to the host via ssh: ssh: connect to host 1.1.1.1 port 22: Connection timed out\r\n', 158 | 'unreachable': True}, 159 | '192.168.1.100': {'_ansible_no_log': False, 160 | '_ansible_parsed': True, 161 | u'_ansible_verbose_override': True, 162 | u'changed': False, 163 | 'invocation': { 164 | u'module_args': 165 | {u'fact_path':u'/etc/ansible/facts.d', 166 | u'filter': u'*', 167 | u'gather_subset': [u'all'], 168 | u'gather_timeout': 10}, 169 | 'module_name': u'setup'}}}, 170 | 'task': {'name': 'setup'}}, 171 | {'hosts': {'192.168.1.100': {'_ansible_no_log': False, 172 | '_ansible_parsed': True, 173 | u'changed': True, 174 | u'cmd': u'uptime', 175 | u'delta': u'0:00:00.006512', 176 | u'end': u'2017-01-16 16:56:33.838901', 177 | 'invocation': {u'module_args': { 178 | u'_raw_params': u'uptime', 179 | u'_uses_shell': True, 180 | u'chdir': None, 181 | u'creates': None, 182 | u'executable': None, 183 | u'removes': None, 184 | u'warn': True}, 185 | 'module_name': u'command'}, 186 | u'rc': 0, 187 | u'start': u'2017-01-16 16:56:33.832389', 188 | u'stderr': u'', 189 | u'stdout': u' 16:56:33 up 570 days, 1:49, 1 user, load average: 0.01, 0.02, 0.05', 190 | 'stdout_lines': [u' 16:56:33 up 570 days, 1:49, 1 user, load average: 0.01, 0.02, 0.05'], 191 | u'warnings': []}}, 192 | 'task': {'name': u'exec uptime'}}]}], 193 | 'stats': {'1.1.1.1': {'changed': 0, 194 | 'failures': 0, 195 | 'ok': 0, 196 | 'skipped': 0, 197 | 'unreachable': 1}, 198 | '192.168.1.100': {'changed': 1, 199 | 'failures': 0, 200 | 'ok': 2, 201 | 'skipped': 0, 202 | 'unreachable': 0}}} 203 | ``` 204 | 205 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ["runner", "playbook_runner"] 3 | -------------------------------------------------------------------------------- /myinventory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf8 3 | 4 | import os 5 | from ansible.inventory import Inventory 6 | from ansible.inventory.host import Host 7 | from ansible.inventory.group import Group 8 | from ansible.parsing.dataloader import DataLoader 9 | from ansible.vars import VariableManager 10 | from ansible.compat.six import string_types 11 | from ansible.parsing.utils.addresses import parse_address 12 | from ansible.utils.vars import combine_vars 13 | from ansible.inventory.dir import InventoryDirectory, get_file_parser 14 | from ansible import constants as C 15 | from ansible.errors import AnsibleError 16 | from ansible.utils.unicode import to_unicode, to_str 17 | from ansible.plugins import vars_loader 18 | from ansible.compat.six import iteritems 19 | 20 | 21 | __all__ = ["MyInventory", ] 22 | 23 | HOSTS_PATTERNS_CACHE = {} 24 | 25 | class MyInventory(Inventory): 26 | """ 27 | this is my ansible inventory object. 28 | """ 29 | def __init__(self, host_list=None): 30 | """ 31 | host_list的数据格式是一个列表字典,比如 32 | { 33 | "group1": { 34 | "hosts": [{"hostname": "10.10.10.10", "port": "22", 35 | "username": "test", "password": "mypass"}, ...] 36 | "vars": {"var1": value1, "var2": value2, ...} 37 | } 38 | } 39 | 40 | 如果你只传入1个列表,则不能加载主机变量 41 | ['1.1.1.1', '2.2.2.2'...] 42 | or 43 | "1.1.1.1," 44 | or 45 | "1.1.1.1,2.2.2.2" 46 | """ 47 | self.host_list = host_list or [] 48 | self.loader = DataLoader() 49 | self.variable_manager = VariableManager() 50 | super(MyInventory, self).__init__(self.loader, self.variable_manager, host_list=[]) 51 | self.clear_pattern_cache() 52 | 53 | # perform my `parse_inventory()` 54 | self.parse_inventory(host_list) 55 | 56 | def parse_inventory(self, host_list): 57 | 58 | if isinstance(host_list, string_types): 59 | if "," in host_list: 60 | host_list = [ h.strip() for h in host_list.split(',') if h and h.strip() ] 61 | else: 62 | host_list = [ host_list ] 63 | 64 | 65 | self.parser = None 66 | 67 | # Always create the 'all' and 'ungrouped' groups, even if host_list is 68 | # empty: in this case we will subsequently an the implicit 'localhost' to it. 69 | 70 | ungrouped = Group('ungrouped') 71 | all = Group('all') 72 | all.add_child_group(ungrouped) 73 | 74 | self.groups = dict(all=all, ungrouped=ungrouped) 75 | 76 | if host_list is None: 77 | pass 78 | 79 | elif isinstance(host_list, list): 80 | for h in host_list: 81 | try: 82 | (host, port) = parse_address(h, allow_ranges=False) 83 | except AnsibleError as e: 84 | raise AnsibleError("Unable to parse address from hostname, leaving unchanged: %s" % to_unicode(e)) 85 | else: 86 | host = h 87 | port = None 88 | 89 | new_host = Host(host, port) 90 | if h in C.LOCALHOST: 91 | # set default localhost from inventory to avoid creating an implicit one. Last localhost defined 'wins'. 92 | if self.localhost is not None: 93 | raise AnsibleError("A duplicate localhost-like entry was found (%s). First found localhost was %s" % (h, self.localhost.name)) 94 | # display.vvvv("Set default localhost to %s" % h) 95 | self.localhost = new_host 96 | all.add_host(new_host) 97 | 98 | # custom use InventoryDictParser() 99 | elif isinstance(host_list, dict): 100 | self.parser = InventoryDictParser(loader=self._loader, groups=self.groups, dictdata=host_list) 101 | 102 | elif self._loader.path_exists(host_list): 103 | #TODO: switch this to a plugin loader and a 'condition' per plugin on which it should be tried, restoring 'inventory pllugins' 104 | if self.is_directory(host_list): 105 | # Ensure basedir is inside the directory 106 | host_list = os.path.join(self.host_list, "") 107 | self.parser = InventoryDirectory(loader=self._loader, groups=self.groups, filename=host_list) 108 | else: 109 | self.parser = get_file_parser(host_list, self.groups, self._loader) 110 | vars_loader.add_directory(self._basedir, with_subdir=True) 111 | 112 | if not self.parser: 113 | # should never happen, but JIC 114 | raise AnsibleError("Unable to parse %s as an inventory source" % host_list) 115 | else: 116 | raise AnsibleError( 117 | "host_list parse error, please correct your data source") 118 | 119 | self._vars_plugins = [ x for x in vars_loader.all(self) ] 120 | 121 | # set group vars from group_vars/ files and vars plugins 122 | for g in self.groups: 123 | group = self.groups[g] 124 | group.vars = combine_vars(group.vars, self.get_group_variables(group.name)) 125 | 126 | # get host vars from host_vars/ files and vars plugins 127 | for host in self.get_hosts(): 128 | host.vars = combine_vars(host.vars, self.get_host_variables(host.name)) 129 | self.get_host_vars(host) 130 | 131 | 132 | class InventoryDictParser(object): 133 | """ 134 | Host inventory parser for ansible using Dict data. as inventory scripts. 135 | """ 136 | def __init__(self, loader, groups=None, dictdata=None): 137 | self._loader = loader 138 | self.groups = groups or {} 139 | self.dictdata = dictdata 140 | self.host_vars_from_top = None 141 | self._parse() 142 | 143 | def _parse(self): 144 | all_hosts = {} 145 | 146 | group = None 147 | for (group_name, data) in self.dictdata.items(): 148 | 149 | if group_name == '_meta': 150 | if 'hostvars' in data: 151 | self.host_vars_from_top = data['hostvars'] 152 | continue 153 | 154 | if group_name not in self.groups: 155 | self.groups[group_name] = Group(group_name) 156 | 157 | group = self.groups[group_name] 158 | host = None 159 | 160 | # struct_1 "group": [ip1, ip2, ...] 161 | if not isinstance(data, dict): 162 | data = {'hosts': data} 163 | # struct_2 "ip": {'var':'value1', ...} 164 | # is not those subkeys, then simplified syntax, host with vars 165 | elif not any(k in data for k in ('hosts', 'vars', 'children')): 166 | data = {'hosts': [group_name], 'vars': data} 167 | 168 | if 'hosts' in data: 169 | if not isinstance(data['hosts'], list): 170 | raise AnsibleError("You defined a group \"%s\" with bad " 171 | "data for the host list:\n %s" % (group_name, data)) 172 | 173 | for hostname in data['hosts']: 174 | if hostname not in all_hosts: 175 | all_hosts[hostname] = Host(hostname) 176 | host = all_hosts[hostname] 177 | group.add_host(host) 178 | 179 | if 'vars' in data: 180 | if not isinstance(data['vars'], dict): 181 | raise AnsibleError("You defined a group \"%s\" with bad " 182 | "data for variables:\n %s" % (group_name, data)) 183 | 184 | for k, v in iteritems(data['vars']): 185 | group.set_variable(k, v) 186 | 187 | # Separate loop to ensure all groups are defined 188 | for (group_name, data) in self.dictdata.items(): 189 | if group_name == '_meta': 190 | continue 191 | if isinstance(data, dict) and 'children' in data: 192 | for child_name in data['children']: 193 | if child_name in self.groups: 194 | self.groups[group_name].add_child_group(self.groups[child_name]) 195 | 196 | # Finally, add all top-level groups as children of 'all'. 197 | # We exclude ungrouped here because it was already added as a child of 198 | # 'all' at the time it was created. 199 | 200 | for group in self.groups.values(): 201 | if group.depth == 0 and group.name not in ('all', 'ungrouped'): 202 | self.groups['all'].add_child_group(group) 203 | 204 | 205 | 206 | def get_host_variables(self, host): 207 | if self.host_vars_from_top is None: 208 | return dict() 209 | else: 210 | try: 211 | got = self.host_vars_from_top.get(host.name, {}) 212 | except AttributeError as e: 213 | raise AnsibleError("Improperly formated host information for %s: %s" % (host.name,to_str(e))) 214 | return got 215 | 216 | 217 | if __name__ == "__main__": 218 | host_list = { 219 | "group1": ['1.1.1.1'], 220 | "group2": { 221 | "hosts": ["2.2.2.2"], 222 | "vars": {"var2": "var_value2"} 223 | }, 224 | "3.3.3.3":{ 225 | "ansible_ssh_host": "3.3.3.3", 226 | "3vars": "3value" 227 | }, 228 | "_meta":{"hostvars":{}} 229 | } 230 | 231 | host_list1 = "1.1.1.1" 232 | host_list2 = ["1.1.1.1","2.2.2.2"] 233 | 234 | hosts_source = host_list 235 | myhosts = MyInventory(hosts_source) 236 | 237 | print "groups:", myhosts.list_groups() 238 | print "all.child_groups:", myhosts.groups["all"].child_groups 239 | print "all:", myhosts.list_hosts("all") 240 | print "*:", myhosts.list_hosts("*") 241 | 242 | print "all group hosts:", myhosts.groups["all"].get_hosts() 243 | print "pattern_cache:", myhosts._pattern_cache 244 | 245 | if isinstance(hosts_source, dict): 246 | print "group1:", myhosts.list_hosts("group1") 247 | print "group2:", myhosts.list_hosts("group2") 248 | 249 | print "group1 vars:", myhosts.get_group_vars(myhosts.groups["group1"]) 250 | print myhosts.groups["group1"].vars 251 | 252 | print "group2 vars:", myhosts.get_group_vars(myhosts.groups["group2"]) 253 | print myhosts.groups["group2"].vars 254 | -------------------------------------------------------------------------------- /playbook_runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf8 3 | 4 | import os 5 | from collections import namedtuple 6 | from ansible.parsing.dataloader import DataLoader 7 | from ansible.vars import VariableManager 8 | from ansible.executor.playbook_executor import PlaybookExecutor 9 | from ansible.plugins.callback import CallbackBase 10 | import ansible.constants as C 11 | from ansible.errors import AnsibleError 12 | from ansible.utils.vars import load_extra_vars 13 | from ansible.utils.vars import load_options_vars 14 | from myinventory import MyInventory 15 | 16 | 17 | __all__ = ['PlaybookRunner'] 18 | 19 | Options = namedtuple('Options', [ 20 | 'listtags', 'listtasks', 'listhosts', 'syntax', 'connection', 21 | 'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout', 22 | 'ssh_common_args', 'ssh_extra_args', 'sftp_extra_args', 'scp_extra_args', 23 | 'become', 'become_method', 'become_user', 'verbosity', 'check', 'extra_vars']) 24 | 25 | 26 | class CallbackModule(CallbackBase): 27 | """ 28 | Custom callback model for handlering the output data of 29 | execute playbook file, 30 | 31 | Base on the build-in callback plugins of ansible which named `json`. 32 | """ 33 | 34 | CALLBACK_VERSION = 2.0 35 | CALLBACK_TYPE = 'stdout' 36 | CALLBACK_NAME = 'Dict' 37 | 38 | def __init__(self, display=None): 39 | super(CallbackModule, self).__init__(display) 40 | self.results = [] 41 | self.output = "" 42 | self.item_results = {} # {"host": []} 43 | 44 | def _new_play(self, play): 45 | return { 46 | 'play': { 47 | 'name': play.name, 48 | 'id': str(play._uuid) 49 | }, 50 | 'tasks': [] 51 | } 52 | 53 | def _new_task(self, task): 54 | return { 55 | 'task': { 56 | 'name': task.get_name(), 57 | }, 58 | 'hosts': {} 59 | } 60 | 61 | def v2_playbook_on_no_hosts_matched(self): 62 | self.output = "skipping: No match hosts." 63 | 64 | def v2_playbook_on_no_hosts_remaining(self): 65 | pass 66 | 67 | def v2_playbook_on_task_start(self, task, is_conditional): 68 | self.results[-1]['tasks'].append(self._new_task(task)) 69 | 70 | def v2_playbook_on_play_start(self, play): 71 | self.results.append(self._new_play(play)) 72 | 73 | def v2_playbook_on_stats(self, stats): 74 | hosts = sorted(stats.processed.keys()) 75 | summary = {} 76 | for h in hosts: 77 | s = stats.summarize(h) 78 | summary[h] = s 79 | 80 | if self.output: 81 | pass 82 | else: 83 | self.output = { 84 | 'plays': self.results, 85 | 'stats': summary 86 | } 87 | 88 | def gather_result(self, res): 89 | if res._task.loop and "results" in res._result and res._host.name in self.item_results: 90 | res._result.update({"results": self.item_results[res._host.name]}) 91 | del self.item_results[res._host.name] 92 | 93 | self.results[-1]['tasks'][-1]['hosts'][res._host.name] = res._result 94 | 95 | def v2_runner_on_ok(self, res, **kwargs): 96 | if "ansible_facts" in res._result: 97 | del res._result["ansible_facts"] 98 | 99 | self.gather_result(res) 100 | 101 | def v2_runner_on_failed(self, res, **kwargs): 102 | self.gather_result(res) 103 | 104 | def v2_runner_on_unreachable(self, res, **kwargs): 105 | self.gather_result(res) 106 | 107 | def v2_runner_on_skipped(self, res, **kwargs): 108 | self.gather_result(res) 109 | 110 | def gather_item_result(self, res): 111 | self.item_results.setdefault(res._host.name, []).append(res._result) 112 | 113 | def v2_runner_item_on_ok(self, res): 114 | self.gather_item_result(res) 115 | 116 | def v2_runner_item_on_failed(self, res): 117 | self.gather_item_result(res) 118 | 119 | def v2_runner_item_on_skipped(self, res): 120 | self.gather_item_result(res) 121 | 122 | 123 | 124 | class PlaybookRunner(object): 125 | """ 126 | The plabybook API. 127 | """ 128 | 129 | def __init__( 130 | self, 131 | hosts=None, # a list or dynamic-hosts, 132 | # default is /etc/ansible/hosts 133 | playbook_path=None, # * a playbook file 134 | forks=C.DEFAULT_FORKS, 135 | listtags=False, 136 | listtasks=False, 137 | listhosts=False, 138 | syntax=False, 139 | module_path=None, 140 | remote_user='root', 141 | timeout=C.DEFAULT_TIMEOUT, 142 | ssh_common_args=None, 143 | ssh_extra_args=None, 144 | sftp_extra_args=None, 145 | scp_extra_args=None, 146 | become=True, 147 | become_method=None, 148 | become_user="root", 149 | verbosity=None, 150 | extra_vars=None, 151 | connection_type="ssh", 152 | passwords=None, 153 | private_key_file=None, 154 | check=False 155 | ): 156 | 157 | C.RETRY_FILES_ENABLED = False 158 | self.callbackmodule = CallbackModule() 159 | if playbook_path is None or not os.path.exists(playbook_path): 160 | raise AnsibleError( 161 | "Not Found the playbook file: %s." % playbook_path) 162 | self.playbook_path = playbook_path 163 | self.loader = DataLoader() 164 | self.variable_manager = VariableManager() 165 | self.passwords = passwords or {} 166 | self.inventory = MyInventory(host_list=hosts) 167 | 168 | self.options = Options( 169 | listtags=listtags, 170 | listtasks=listtasks, 171 | listhosts=listhosts, 172 | syntax=syntax, 173 | timeout=timeout, 174 | connection=connection_type, 175 | module_path=module_path, 176 | forks=forks, 177 | remote_user=remote_user, 178 | private_key_file=private_key_file, 179 | ssh_common_args=ssh_common_args or "", 180 | ssh_extra_args=ssh_extra_args or "", 181 | sftp_extra_args=sftp_extra_args, 182 | scp_extra_args=scp_extra_args, 183 | become=become, 184 | become_method=become_method, 185 | become_user=become_user, 186 | verbosity=verbosity, 187 | extra_vars=extra_vars or [], 188 | check=check 189 | ) 190 | 191 | self.variable_manager.extra_vars = load_extra_vars(loader=self.loader, options=self.options) 192 | self.variable_manager.options_vars = load_options_vars(self.options) 193 | 194 | self.variable_manager.set_inventory(self.inventory) 195 | self.runner = PlaybookExecutor( 196 | playbooks=[self.playbook_path], 197 | inventory=self.inventory, 198 | variable_manager=self.variable_manager, 199 | loader=self.loader, 200 | options=self.options, 201 | passwords=self.passwords 202 | ) 203 | if self.runner._tqm: 204 | self.runner._tqm._stdout_callback = self.callbackmodule 205 | 206 | def run(self): 207 | if not self.inventory.list_hosts("all"): 208 | raise AnsibleError("Inventory is empty.") 209 | 210 | self.runner.run() 211 | return self.callbackmodule.output 212 | -------------------------------------------------------------------------------- /runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf8 3 | 4 | 5 | from collections import namedtuple 6 | from ansible.parsing.dataloader import DataLoader 7 | from ansible.vars import VariableManager 8 | from ansible.playbook.play import Play 9 | from ansible.executor.task_queue_manager import TaskQueueManager 10 | from ansible.plugins.callback import CallbackBase 11 | import ansible.constants as C 12 | from ansible.errors import AnsibleError 13 | from ansible.utils.vars import load_extra_vars 14 | from ansible.utils.vars import load_options_vars 15 | 16 | from myinventory import MyInventory 17 | 18 | __all__ = ["Runner"] 19 | 20 | # free to report host to `known_hosts` file. 21 | C.HOST_KEY_CHECKING = False 22 | 23 | # class Options 24 | Options = namedtuple("Options", [ 25 | 'connection', 'module_path', 'private_key_file', "remote_user", "timeout", 26 | 'forks', 'become', 'become_method', 'become_user', 'check', "extra_vars", 27 | ] 28 | ) 29 | 30 | 31 | class ResultCallback(CallbackBase): 32 | """ 33 | Custom Callback 34 | """ 35 | def __init__(self): 36 | self.result_q = dict(contacted={}, dark={}) 37 | 38 | def gather_result(self, n, res): 39 | self.result_q[n].update({res._host.name: res._result}) 40 | 41 | def v2_runner_on_ok(self, result): 42 | self.gather_result("contacted", result) 43 | 44 | def v2_runner_on_failed(self, result, ignore_errors=False): 45 | self.gather_result("dark", result) 46 | 47 | def v2_runner_on_unreachable(self, result): 48 | self.gather_result("dark", result) 49 | 50 | def v2_runner_on_skipped(self, result): 51 | self.gather_result("dark", result) 52 | 53 | def v2_playbook_on_task_start(self, task, is_conditional): 54 | pass 55 | 56 | def v2_playbook_on_play_start(self, play): 57 | pass 58 | 59 | 60 | 61 | class Runner(object): 62 | """ 63 | 仿照ansible1.9 的python API,制作的ansible2.0 API的简化版本。 64 | 参数说明: 65 | inventory:: 仓库对象,可以是列表,逗号间隔的ip字符串,可执行文件. 默认/etc/ansible/hosts 66 | module_name:: 指定要使用的模块 67 | module_args:: 模块参数 68 | forks:: 并发数量, 默认5 69 | timeout:: 连接等待超时时间,默认10秒 70 | pattern:: 模式匹配,指定要连接的主机名, 默认all 71 | remote_user:: 指定连接用户, 默认root 72 | private_key_files:: 指定私钥文件 73 | """ 74 | def __init__( 75 | self, 76 | hosts=C.DEFAULT_HOST_LIST, 77 | module_name=C.DEFAULT_MODULE_NAME, # * command 78 | module_args=C.DEFAULT_MODULE_ARGS, # * 'cmd args' 79 | forks=C.DEFAULT_FORKS, # 5 80 | timeout=C.DEFAULT_TIMEOUT, # SSH timeout = 10s 81 | pattern="all", # all 82 | remote_user=C.DEFAULT_REMOTE_USER, # root 83 | module_path=None, # dirs of custome modules 84 | connection_type="smart", 85 | become=None, 86 | become_method=None, 87 | become_user=None, 88 | check=False, 89 | passwords=None, 90 | extra_vars = None, 91 | private_key_file=None 92 | ): 93 | 94 | # storage & defaults 95 | self.pattern = pattern 96 | self.variable_manager = VariableManager() 97 | self.loader = DataLoader() 98 | self.module_name = module_name 99 | self.module_args = module_args 100 | self.check_module_args() 101 | self.gather_facts = 'no' 102 | self.resultcallback = ResultCallback() 103 | self.options = Options( 104 | connection=connection_type, 105 | timeout=timeout, 106 | module_path=module_path, 107 | forks=forks, 108 | become=become, 109 | become_method=become_method, 110 | become_user=become_user, 111 | check=check, 112 | remote_user=remote_user, 113 | extra_vars=extra_vars or [], 114 | private_key_file=private_key_file, 115 | ) 116 | 117 | self.variable_manager.extra_vars = load_extra_vars(loader=self.loader, options=self.options) 118 | self.variable_manager.options_vars = load_options_vars(self.options) 119 | 120 | self.passwords = passwords or {} 121 | self.inventory = MyInventory(host_list=hosts) 122 | self.variable_manager.set_inventory(self.inventory) 123 | 124 | self.play_source = dict( 125 | name="Ansible Ad-hoc", 126 | hosts=self.pattern, 127 | gather_facts=self.gather_facts, 128 | tasks=[dict(action=dict( 129 | module=self.module_name, args=self.module_args))] 130 | ) 131 | 132 | self.play = Play().load( 133 | self.play_source, variable_manager=self.variable_manager, 134 | loader=self.loader) 135 | 136 | self.runner = TaskQueueManager( 137 | inventory=self.inventory, 138 | variable_manager=self.variable_manager, 139 | loader=self.loader, 140 | options=self.options, 141 | passwords=self.passwords, 142 | stdout_callback=self.resultcallback 143 | ) 144 | 145 | # ** end __init__() ** 146 | 147 | def run(self): 148 | if not self.inventory.list_hosts("all"): 149 | raise AnsibleError("Inventory is empty.") 150 | 151 | if not self.inventory.list_hosts(self.pattern): 152 | raise AnsibleError( 153 | "pattern: %s dose not match any hosts." % self.pattern) 154 | 155 | try: 156 | self.runner.run(self.play) 157 | except Exception as e: 158 | raise Exception(e) 159 | else: 160 | return self.resultcallback.result_q 161 | finally: 162 | if self.runner: 163 | self.runner.cleanup() 164 | if self.loader: 165 | self.loader.cleanup_all_tmp_files() 166 | 167 | 168 | def check_module_args(self): 169 | if self.module_name in C.MODULE_REQUIRE_ARGS and not self.module_args: 170 | err = "No argument passed to '%s' module." % self.module_name 171 | raise AnsibleError(err) 172 | -------------------------------------------------------------------------------- /test/debug.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Test the plabybook API. 4 | hosts: all 5 | remote_user: root 6 | gather_facts: yes 7 | tasks: 8 | - name: exec uptime 9 | shell: uptime 10 | -------------------------------------------------------------------------------- /test/playbook_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf8 3 | 4 | 5 | import sys 6 | sys.path.append("../") 7 | from playbook_runner import PlaybookRunner 8 | from pprint import pprint 9 | 10 | host_dict = { 11 | "group1": { 12 | 'hosts': ["192.168.1.100", "1.1.1.1"], 13 | 'vars': {'host': 'var_value'} 14 | }, 15 | "_meta":{ 16 | "hostvars":{ 17 | "192.168.1.100":{ 18 | "zone_dirs": ["/home/gjobs3","/home/gjobs2"] 19 | } 20 | } 21 | } 22 | } 23 | 24 | 25 | runner = PlaybookRunner( 26 | # playbook_path="two_play.yml", 27 | playbook_path="debug.yml", 28 | hosts=host_dict, 29 | ) 30 | 31 | 32 | try: 33 | results = runner.run() 34 | pprint(results) 35 | except Exception as e: 36 | print e 37 | -------------------------------------------------------------------------------- /test/runner_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf8 3 | 4 | import sys 5 | sys.path.append("../") 6 | 7 | from runner import Runner 8 | from pprint import pprint 9 | 10 | host_dict = { 11 | "group1": { 12 | 'hosts': ["192.168.1.100", "1.1.1.1"], 13 | 'vars': {'host': 'var_value'} 14 | }, 15 | "_meta": { 16 | "hostvars": { 17 | "192.168.1.100": {"hosts": "hostvalue"} 18 | } 19 | } 20 | } 21 | 22 | runner = Runner( 23 | module_name="shell", 24 | module_args="uptime", 25 | remote_user="root", 26 | pattern="all", 27 | hosts=host_dict, 28 | ) 29 | 30 | pprint(runner.run()) 31 | -------------------------------------------------------------------------------- /test/two_play.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Test the plabybook API. 4 | hosts: all 5 | remote_user: root 6 | gather_facts: yes 7 | tasks: 8 | - name: uptime 9 | shell: uptime 10 | 11 | 12 | - name: Second test 13 | hosts: all 14 | remote_user: root 15 | gather_facts: yes 16 | tasks: 17 | - name: hostname 18 | shell: hostname 19 | 20 | --------------------------------------------------------------------------------