1# 2# Author: Bo Maryniuk <bo@suse.de> 3# 4""" 5Ansible Support 6=============== 7 8This module can have an optional minion-level 9configuration in /etc/salt/minion.d/ as follows: 10 11 ansible_timeout: 1200 12 13The timeout is how many seconds Salt should wait for 14any Ansible module to respond. 15""" 16 17import fnmatch 18import json 19import logging 20import subprocess 21import sys 22from tempfile import NamedTemporaryFile 23 24import salt.utils.decorators.path 25import salt.utils.json 26import salt.utils.path 27import salt.utils.platform 28import salt.utils.stringutils 29import salt.utils.timed_subprocess 30import salt.utils.yaml 31from salt.exceptions import CommandExecutionError 32 33# Function alias to make sure not to shadow built-in's 34__func_alias__ = {"list_": "list"} 35 36__virtualname__ = "ansible" 37 38log = logging.getLogger(__name__) 39 40INVENTORY = """ 41hosts: 42 vars: 43 ansible_connection: local 44""" 45DEFAULT_TIMEOUT = 1200 # seconds (20 minutes) 46 47__load__ = __non_ansible_functions__ = ["help", "list_", "call", "playbooks"][:] 48 49 50def _set_callables(modules): 51 """ 52 Set all Ansible modules callables 53 :return: 54 """ 55 56 def _set_function(cmd_name, doc): 57 """ 58 Create a Salt function for the Ansible module. 59 """ 60 61 def _cmd(*args, **kwargs): 62 """ 63 Call an Ansible module as a function from the Salt. 64 """ 65 return call(cmd_name, *args, **kwargs) 66 67 _cmd.__doc__ = doc 68 return _cmd 69 70 for mod, doc in modules.items(): 71 __load__.append(mod) 72 setattr(sys.modules[__name__], mod, _set_function(mod, doc)) 73 74 75def __virtual__(): 76 if salt.utils.platform.is_windows(): 77 return False, "The ansiblegate module isn't supported on Windows" 78 ansible_bin = salt.utils.path.which("ansible") 79 if not ansible_bin: 80 return False, "The 'ansible' binary was not found." 81 ansible_doc_bin = salt.utils.path.which("ansible-doc") 82 if not ansible_doc_bin: 83 return False, "The 'ansible-doc' binary was not found." 84 ansible_playbook_bin = salt.utils.path.which("ansible-playbook") 85 if not ansible_playbook_bin: 86 return False, "The 'ansible-playbook' binary was not found." 87 88 proc = subprocess.run( 89 [ansible_doc_bin, "--list", "--json", "--type=module"], 90 stdout=subprocess.PIPE, 91 stderr=subprocess.PIPE, 92 check=False, 93 shell=False, 94 universal_newlines=True, 95 ) 96 if proc.returncode != 0: 97 return ( 98 False, 99 "Failed to get the listing of ansible modules:\n{}".format(proc.stderr), 100 ) 101 102 ansible_module_listing = salt.utils.json.loads(proc.stdout) 103 for key in list(ansible_module_listing): 104 if key.startswith("ansible."): 105 # Fyi, str.partition() is faster than str.replace() 106 _, _, alias = key.partition(".") 107 ansible_module_listing[alias] = ansible_module_listing[key] 108 _set_callables(ansible_module_listing) 109 return __virtualname__ 110 111 112def help(module=None, *args): 113 """ 114 Display help on Ansible standard module. 115 116 :param module: The module to get the help 117 118 CLI Example: 119 120 .. code-block:: bash 121 122 salt * ansible.help ping 123 """ 124 if not module: 125 raise CommandExecutionError( 126 "Please tell me what module you want to have helped with. " 127 'Or call "ansible.list" to know what is available.' 128 ) 129 130 ansible_doc_bin = salt.utils.path.which("ansible-doc") 131 132 proc = subprocess.run( 133 [ansible_doc_bin, "--json", "--type=module", module], 134 stdout=subprocess.PIPE, 135 stderr=subprocess.PIPE, 136 check=True, 137 shell=False, 138 universal_newlines=True, 139 ) 140 data = salt.utils.json.loads(proc.stdout) 141 doc = data[next(iter(data))] 142 if not args: 143 ret = doc["doc"] 144 for section in ("examples", "return", "metadata"): 145 section_data = doc.get(section) 146 if section_data: 147 ret[section] = section_data 148 else: 149 ret = {} 150 for arg in args: 151 info = doc.get(arg) 152 if info is not None: 153 ret[arg] = info 154 return ret 155 156 157def list_(pattern=None): 158 """ 159 Lists available modules. 160 161 CLI Example: 162 163 .. code-block:: bash 164 165 salt * ansible.list 166 salt * ansible.list '*win*' # To get all modules matching 'win' on it's name 167 """ 168 if pattern is None: 169 module_list = set(__load__) 170 module_list.discard(set(__non_ansible_functions__)) 171 return sorted(module_list) 172 return sorted(fnmatch.filter(__load__, pattern)) 173 174 175def call(module, *args, **kwargs): 176 """ 177 Call an Ansible module by invoking it. 178 179 :param module: the name of the module. 180 :param args: Arguments to pass to the module 181 :param kwargs: keywords to pass to the module 182 183 CLI Example: 184 185 .. code-block:: bash 186 187 salt * ansible.call ping data=foobar 188 """ 189 190 module_args = [] 191 for arg in args: 192 module_args.append(salt.utils.json.dumps(arg)) 193 194 _kwargs = {} 195 for _kw in kwargs.get("__pub_arg", []): 196 if isinstance(_kw, dict): 197 _kwargs = _kw 198 break 199 else: 200 _kwargs = {k: v for (k, v) in kwargs.items() if not k.startswith("__pub")} 201 202 for key, value in _kwargs.items(): 203 module_args.append("{}={}".format(key, salt.utils.json.dumps(value))) 204 205 with NamedTemporaryFile(mode="w") as inventory: 206 207 ansible_binary_path = salt.utils.path.which("ansible") 208 log.debug("Calling ansible module %r", module) 209 try: 210 proc_exc = subprocess.run( 211 [ 212 ansible_binary_path, 213 "localhost", 214 "--limit", 215 "127.0.0.1", 216 "-m", 217 module, 218 "-a", 219 " ".join(module_args), 220 "-i", 221 inventory.name, 222 ], 223 stdout=subprocess.PIPE, 224 stderr=subprocess.PIPE, 225 timeout=__opts__.get("ansible_timeout", DEFAULT_TIMEOUT), 226 universal_newlines=True, 227 check=True, 228 shell=False, 229 ) 230 231 original_output = proc_exc.stdout 232 proc_out = original_output.splitlines() 233 if proc_out[0].endswith("{"): 234 proc_out[0] = "{" 235 try: 236 out = salt.utils.json.loads("\n".join(proc_out)) 237 except ValueError as exc: 238 out = { 239 "Error": proc_exc.stderr or str(exc), 240 "Output": original_output, 241 } 242 return out 243 elif proc_out[0].endswith(">>"): 244 out = {"output": "\n".join(proc_out[1:])} 245 else: 246 out = {"output": original_output} 247 248 except subprocess.CalledProcessError as exc: 249 out = {"Exitcode": exc.returncode, "Error": exc.stderr or str(exc)} 250 if exc.stdout: 251 out["Given JSON output"] = exc.stdout 252 return out 253 254 for key in ("invocation", "changed"): 255 out.pop(key, None) 256 257 return out 258 259 260@salt.utils.decorators.path.which("ansible-playbook") 261def playbooks( 262 playbook, 263 rundir=None, 264 check=False, 265 diff=False, 266 extra_vars=None, 267 flush_cache=False, 268 forks=5, 269 inventory=None, 270 limit=None, 271 list_hosts=False, 272 list_tags=False, 273 list_tasks=False, 274 module_path=None, 275 skip_tags=None, 276 start_at_task=None, 277 syntax_check=False, 278 tags=None, 279 playbook_kwargs=None, 280): 281 """ 282 Run Ansible Playbooks 283 284 :param playbook: Which playbook to run. 285 :param rundir: Directory to run `ansible-playbook` in. (Default: None) 286 :param check: don't make any changes; instead, try to predict some 287 of the changes that may occur (Default: False) 288 :param diff: when changing (small) files and templates, show the 289 differences in those files; works great with --check 290 (default: False) 291 :param extra_vars: set additional variables as key=value or YAML/JSON, if 292 filename prepend with @, (default: None) 293 :param flush_cache: clear the fact cache for every host in inventory 294 (default: False) 295 :param forks: specify number of parallel processes to use 296 (Default: 5) 297 :param inventory: specify inventory host path or comma separated host 298 list. (Default: None) (Ansible's default is /etc/ansible/hosts) 299 :param limit: further limit selected hosts to an additional pattern (Default: None) 300 :param list_hosts: outputs a list of matching hosts; does not execute anything else 301 (Default: False) 302 :param list_tags: list all available tags (Default: False) 303 :param list_tasks: list all tasks that would be executed (Default: False) 304 :param module_path: prepend colon-separated path(s) to module library. (Default: None) 305 :param skip_tags: only run plays and tasks whose tags do not match these 306 values (Default: False) 307 :param start_at_task: start the playbook at the task matching this name (Default: None) 308 :param: syntax_check: perform a syntax check on the playbook, but do not execute it 309 (Default: False) 310 :param tags: only run plays and tasks tagged with these values (Default: None) 311 312 :return: Playbook return 313 314 CLI Example: 315 316 .. code-block:: bash 317 318 salt 'ansiblehost' ansible.playbook playbook=/srv/playbooks/play.yml 319 """ 320 command = ["ansible-playbook", playbook] 321 if check: 322 command.append("--check") 323 if diff: 324 command.append("--diff") 325 if isinstance(extra_vars, dict): 326 command.append("--extra-vars='{}'".format(json.dumps(extra_vars))) 327 elif isinstance(extra_vars, str) and extra_vars.startswith("@"): 328 command.append("--extra-vars={}".format(extra_vars)) 329 if flush_cache: 330 command.append("--flush-cache") 331 if inventory: 332 command.append("--inventory={}".format(inventory)) 333 if limit: 334 command.append("--limit={}".format(limit)) 335 if list_hosts: 336 command.append("--list-hosts") 337 if list_tags: 338 command.append("--list-tags") 339 if list_tasks: 340 command.append("--list-tasks") 341 if module_path: 342 command.append("--module-path={}".format(module_path)) 343 if skip_tags: 344 command.append("--skip-tags={}".format(skip_tags)) 345 if start_at_task: 346 command.append("--start-at-task={}".format(start_at_task)) 347 if syntax_check: 348 command.append("--syntax-check") 349 if tags: 350 command.append("--tags={}".format(tags)) 351 if playbook_kwargs: 352 for key, value in playbook_kwargs.items(): 353 key = key.replace("_", "-") 354 if value is True: 355 command.append("--{}".format(key)) 356 elif isinstance(value, str): 357 command.append("--{}={}".format(key, value)) 358 elif isinstance(value, dict): 359 command.append("--{}={}".format(key, json.dumps(value))) 360 command.append("--forks={}".format(forks)) 361 cmd_kwargs = { 362 "env": {"ANSIBLE_STDOUT_CALLBACK": "json", "ANSIBLE_RETRY_FILES_ENABLED": "0"}, 363 "cwd": rundir, 364 "cmd": " ".join(command), 365 } 366 ret = __salt__["cmd.run_all"](**cmd_kwargs) 367 log.debug("Ansible Playbook Return: %s", ret) 368 retdata = json.loads(ret["stdout"]) 369 if "retcode" in ret: 370 __context__["retcode"] = retdata["retcode"] = ret["retcode"] 371 return retdata 372