1# -*- coding: utf-8 -*- 2# 3# Copyright (c) 2012-2013 Michael DeHaan <michael.dehaan@gmail.com> 4# Copyright (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com> 5# Copyright (c) 2019 Ansible Project 6# Copyright (c) 2020 Felix Fontein <felix@fontein.de> 7# Copyright (c) 2021 Ansible Project 8# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 9 10# Parts taken from ansible.module_utils.basic and ansible.module_utils.common.warnings. 11 12# NOTE: THIS IS ONLY FOR ACTION PLUGINS! 13 14from __future__ import absolute_import, division, print_function 15__metaclass__ = type 16 17 18import abc 19import copy 20import traceback 21 22from ansible import constants as C 23from ansible.errors import AnsibleError 24from ansible.module_utils import six 25from ansible.module_utils.basic import AnsibleFallbackNotFound, SEQUENCETYPE, remove_values 26from ansible.module_utils.common._collections_compat import ( 27 Mapping 28) 29from ansible.module_utils.common.parameters import ( 30 PASS_VARS, 31 PASS_BOOLS, 32) 33from ansible.module_utils.common.validation import ( 34 check_mutually_exclusive, 35 check_required_arguments, 36 check_required_by, 37 check_required_if, 38 check_required_one_of, 39 check_required_together, 40 count_terms, 41 check_type_bool, 42 check_type_bits, 43 check_type_bytes, 44 check_type_float, 45 check_type_int, 46 check_type_jsonarg, 47 check_type_list, 48 check_type_dict, 49 check_type_path, 50 check_type_raw, 51 check_type_str, 52 safe_eval, 53) 54from ansible.module_utils.common.text.formatters import ( 55 lenient_lowercase, 56) 57from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE 58from ansible.module_utils.six import ( 59 binary_type, 60 string_types, 61 text_type, 62) 63from ansible.module_utils.common.text.converters import to_native, to_text 64from ansible.plugins.action import ActionBase 65 66 67try: 68 # For ansible-core 2.11, we can use the ArgumentSpecValidator. We also import 69 # ModuleArgumentSpecValidator since that indicates that the 'classical' approach 70 # will no longer work. 71 from ansible.module_utils.common.arg_spec import ( 72 ArgumentSpecValidator, 73 ModuleArgumentSpecValidator, # noqa 74 ) 75 from ansible.module_utils.errors import UnsupportedError 76 HAS_ARGSPEC_VALIDATOR = True 77except ImportError: 78 # For ansible-base 2.10 and Ansible 2.9, we need to use the 'classical' approach 79 from ansible.module_utils.common.parameters import ( 80 handle_aliases, 81 list_deprecations, 82 list_no_log_values, 83 ) 84 HAS_ARGSPEC_VALIDATOR = False 85 86 87class _ModuleExitException(Exception): 88 def __init__(self, result): 89 super(_ModuleExitException, self).__init__() 90 self.result = result 91 92 93class AnsibleActionModule(object): 94 def __init__(self, action_plugin, argument_spec, bypass_checks=False, 95 mutually_exclusive=None, required_together=None, 96 required_one_of=None, supports_check_mode=False, 97 required_if=None, required_by=None): 98 # Internal data 99 self.__action_plugin = action_plugin 100 self.__warnings = [] 101 self.__deprecations = [] 102 103 # AnsibleModule data 104 self._name = self.__action_plugin._task.action 105 self.argument_spec = argument_spec 106 self.supports_check_mode = supports_check_mode 107 self.check_mode = self.__action_plugin._play_context.check_mode 108 self.bypass_checks = bypass_checks 109 self.no_log = self.__action_plugin._play_context.no_log 110 111 self.mutually_exclusive = mutually_exclusive 112 self.required_together = required_together 113 self.required_one_of = required_one_of 114 self.required_if = required_if 115 self.required_by = required_by 116 self._diff = self.__action_plugin._play_context.diff 117 self._verbosity = self.__action_plugin._display.verbosity 118 self._string_conversion_action = C.STRING_CONVERSION_ACTION 119 120 self.aliases = {} 121 self._legal_inputs = [] 122 self._options_context = list() 123 124 self.params = copy.deepcopy(action_plugin._task.args) 125 self.no_log_values = set() 126 if HAS_ARGSPEC_VALIDATOR: 127 self._validator = ArgumentSpecValidator( 128 self.argument_spec, 129 self.mutually_exclusive, 130 self.required_together, 131 self.required_one_of, 132 self.required_if, 133 self.required_by, 134 ) 135 self._validation_result = self._validator.validate(self.params) 136 self.params.update(self._validation_result.validated_parameters) 137 self.no_log_values.update(self._validation_result._no_log_values) 138 139 try: 140 error = self._validation_result.errors[0] 141 except IndexError: 142 error = None 143 144 # We cannot use ModuleArgumentSpecValidator directly since it uses mechanisms for reporting 145 # warnings and deprecations that do not work in plugins. This is a copy of that code adjusted 146 # for our use-case: 147 for d in self._validation_result._deprecations: 148 self.deprecate( 149 "Alias '{name}' is deprecated. See the module docs for more information".format(name=d['name']), 150 version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name')) 151 152 for w in self._validation_result._warnings: 153 self.warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias'])) 154 155 # Fail for validation errors, even in check mode 156 if error: 157 msg = self._validation_result.errors.msg 158 if isinstance(error, UnsupportedError): 159 msg = "Unsupported parameters for ({name}) {kind}: {msg}".format(name=self._name, kind='module', msg=msg) 160 161 self.fail_json(msg=msg) 162 else: 163 self._set_fallbacks() 164 165 # append to legal_inputs and then possibly check against them 166 try: 167 self.aliases = self._handle_aliases() 168 except (ValueError, TypeError) as e: 169 # Use exceptions here because it isn't safe to call fail_json until no_log is processed 170 raise _ModuleExitException(dict(failed=True, msg="Module alias error: %s" % to_native(e))) 171 172 # Save parameter values that should never be logged 173 self._handle_no_log_values() 174 175 self._check_arguments() 176 177 # check exclusive early 178 if not bypass_checks: 179 self._check_mutually_exclusive(mutually_exclusive) 180 181 self._set_defaults(pre=True) 182 183 self._CHECK_ARGUMENT_TYPES_DISPATCHER = { 184 'str': self._check_type_str, 185 'list': check_type_list, 186 'dict': check_type_dict, 187 'bool': check_type_bool, 188 'int': check_type_int, 189 'float': check_type_float, 190 'path': check_type_path, 191 'raw': check_type_raw, 192 'jsonarg': check_type_jsonarg, 193 'json': check_type_jsonarg, 194 'bytes': check_type_bytes, 195 'bits': check_type_bits, 196 } 197 if not bypass_checks: 198 self._check_required_arguments() 199 self._check_argument_types() 200 self._check_argument_values() 201 self._check_required_together(required_together) 202 self._check_required_one_of(required_one_of) 203 self._check_required_if(required_if) 204 self._check_required_by(required_by) 205 206 self._set_defaults(pre=False) 207 208 # deal with options sub-spec 209 self._handle_options() 210 211 def _handle_aliases(self, spec=None, param=None, option_prefix=''): 212 if spec is None: 213 spec = self.argument_spec 214 if param is None: 215 param = self.params 216 217 # this uses exceptions as it happens before we can safely call fail_json 218 alias_warnings = [] 219 alias_results, self._legal_inputs = handle_aliases(spec, param, alias_warnings=alias_warnings) 220 for option, alias in alias_warnings: 221 self.warn('Both option %s and its alias %s are set.' % (option_prefix + option, option_prefix + alias)) 222 223 deprecated_aliases = [] 224 for i in spec.keys(): 225 if 'deprecated_aliases' in spec[i].keys(): 226 for alias in spec[i]['deprecated_aliases']: 227 deprecated_aliases.append(alias) 228 229 for deprecation in deprecated_aliases: 230 if deprecation['name'] in param.keys(): 231 self.deprecate("Alias '%s' is deprecated. See the module docs for more information" % deprecation['name'], 232 version=deprecation.get('version'), date=deprecation.get('date'), 233 collection_name=deprecation.get('collection_name')) 234 return alias_results 235 236 def _handle_no_log_values(self, spec=None, param=None): 237 if spec is None: 238 spec = self.argument_spec 239 if param is None: 240 param = self.params 241 242 try: 243 self.no_log_values.update(list_no_log_values(spec, param)) 244 except TypeError as te: 245 self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. " 246 "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'}) 247 248 for message in list_deprecations(spec, param): 249 self.deprecate(message['msg'], version=message.get('version'), date=message.get('date'), 250 collection_name=message.get('collection_name')) 251 252 def _check_arguments(self, spec=None, param=None, legal_inputs=None): 253 self._syslog_facility = 'LOG_USER' 254 unsupported_parameters = set() 255 if spec is None: 256 spec = self.argument_spec 257 if param is None: 258 param = self.params 259 if legal_inputs is None: 260 legal_inputs = self._legal_inputs 261 262 for k in list(param.keys()): 263 264 if k not in legal_inputs: 265 unsupported_parameters.add(k) 266 267 for k in PASS_VARS: 268 # handle setting internal properties from internal ansible vars 269 param_key = '_ansible_%s' % k 270 if param_key in param: 271 if k in PASS_BOOLS: 272 setattr(self, PASS_VARS[k][0], self.boolean(param[param_key])) 273 else: 274 setattr(self, PASS_VARS[k][0], param[param_key]) 275 276 # clean up internal top level params: 277 if param_key in self.params: 278 del self.params[param_key] 279 else: 280 # use defaults if not already set 281 if not hasattr(self, PASS_VARS[k][0]): 282 setattr(self, PASS_VARS[k][0], PASS_VARS[k][1]) 283 284 if unsupported_parameters: 285 msg = "Unsupported parameters for (%s) module: %s" % (self._name, ', '.join(sorted(list(unsupported_parameters)))) 286 if self._options_context: 287 msg += " found in %s." % " -> ".join(self._options_context) 288 supported_parameters = list() 289 for key in sorted(spec.keys()): 290 if 'aliases' in spec[key] and spec[key]['aliases']: 291 supported_parameters.append("%s (%s)" % (key, ', '.join(sorted(spec[key]['aliases'])))) 292 else: 293 supported_parameters.append(key) 294 msg += " Supported parameters include: %s" % (', '.join(supported_parameters)) 295 self.fail_json(msg=msg) 296 if self.check_mode and not self.supports_check_mode: 297 self.exit_json(skipped=True, msg="action module (%s) does not support check mode" % self._name) 298 299 def _count_terms(self, check, param=None): 300 if param is None: 301 param = self.params 302 return count_terms(check, param) 303 304 def _check_mutually_exclusive(self, spec, param=None): 305 if param is None: 306 param = self.params 307 308 try: 309 check_mutually_exclusive(spec, param) 310 except TypeError as e: 311 msg = to_native(e) 312 if self._options_context: 313 msg += " found in %s" % " -> ".join(self._options_context) 314 self.fail_json(msg=msg) 315 316 def _check_required_one_of(self, spec, param=None): 317 if spec is None: 318 return 319 320 if param is None: 321 param = self.params 322 323 try: 324 check_required_one_of(spec, param) 325 except TypeError as e: 326 msg = to_native(e) 327 if self._options_context: 328 msg += " found in %s" % " -> ".join(self._options_context) 329 self.fail_json(msg=msg) 330 331 def _check_required_together(self, spec, param=None): 332 if spec is None: 333 return 334 if param is None: 335 param = self.params 336 337 try: 338 check_required_together(spec, param) 339 except TypeError as e: 340 msg = to_native(e) 341 if self._options_context: 342 msg += " found in %s" % " -> ".join(self._options_context) 343 self.fail_json(msg=msg) 344 345 def _check_required_by(self, spec, param=None): 346 if spec is None: 347 return 348 if param is None: 349 param = self.params 350 351 try: 352 check_required_by(spec, param) 353 except TypeError as e: 354 self.fail_json(msg=to_native(e)) 355 356 def _check_required_arguments(self, spec=None, param=None): 357 if spec is None: 358 spec = self.argument_spec 359 if param is None: 360 param = self.params 361 362 try: 363 check_required_arguments(spec, param) 364 except TypeError as e: 365 msg = to_native(e) 366 if self._options_context: 367 msg += " found in %s" % " -> ".join(self._options_context) 368 self.fail_json(msg=msg) 369 370 def _check_required_if(self, spec, param=None): 371 ''' ensure that parameters which conditionally required are present ''' 372 if spec is None: 373 return 374 if param is None: 375 param = self.params 376 377 try: 378 check_required_if(spec, param) 379 except TypeError as e: 380 msg = to_native(e) 381 if self._options_context: 382 msg += " found in %s" % " -> ".join(self._options_context) 383 self.fail_json(msg=msg) 384 385 def _check_argument_values(self, spec=None, param=None): 386 ''' ensure all arguments have the requested values, and there are no stray arguments ''' 387 if spec is None: 388 spec = self.argument_spec 389 if param is None: 390 param = self.params 391 for (k, v) in spec.items(): 392 choices = v.get('choices', None) 393 if choices is None: 394 continue 395 if isinstance(choices, SEQUENCETYPE) and not isinstance(choices, (binary_type, text_type)): 396 if k in param: 397 # Allow one or more when type='list' param with choices 398 if isinstance(param[k], list): 399 diff_list = ", ".join([item for item in param[k] if item not in choices]) 400 if diff_list: 401 choices_str = ", ".join([to_native(c) for c in choices]) 402 msg = "value of %s must be one or more of: %s. Got no match for: %s" % (k, choices_str, diff_list) 403 if self._options_context: 404 msg += " found in %s" % " -> ".join(self._options_context) 405 self.fail_json(msg=msg) 406 elif param[k] not in choices: 407 # PyYaml converts certain strings to bools. If we can unambiguously convert back, do so before checking 408 # the value. If we can't figure this out, module author is responsible. 409 lowered_choices = None 410 if param[k] == 'False': 411 lowered_choices = lenient_lowercase(choices) 412 overlap = BOOLEANS_FALSE.intersection(choices) 413 if len(overlap) == 1: 414 # Extract from a set 415 (param[k],) = overlap 416 417 if param[k] == 'True': 418 if lowered_choices is None: 419 lowered_choices = lenient_lowercase(choices) 420 overlap = BOOLEANS_TRUE.intersection(choices) 421 if len(overlap) == 1: 422 (param[k],) = overlap 423 424 if param[k] not in choices: 425 choices_str = ", ".join([to_native(c) for c in choices]) 426 msg = "value of %s must be one of: %s, got: %s" % (k, choices_str, param[k]) 427 if self._options_context: 428 msg += " found in %s" % " -> ".join(self._options_context) 429 self.fail_json(msg=msg) 430 else: 431 msg = "internal error: choices for argument %s are not iterable: %s" % (k, choices) 432 if self._options_context: 433 msg += " found in %s" % " -> ".join(self._options_context) 434 self.fail_json(msg=msg) 435 436 def safe_eval(self, value, locals=None, include_exceptions=False): 437 return safe_eval(value, locals, include_exceptions) 438 439 def _check_type_str(self, value, param=None, prefix=''): 440 opts = { 441 'error': False, 442 'warn': False, 443 'ignore': True 444 } 445 446 # Ignore, warn, or error when converting to a string. 447 allow_conversion = opts.get(self._string_conversion_action, True) 448 try: 449 return check_type_str(value, allow_conversion) 450 except TypeError: 451 common_msg = 'quote the entire value to ensure it does not change.' 452 from_msg = '{0!r}'.format(value) 453 to_msg = '{0!r}'.format(to_text(value)) 454 455 if param is not None: 456 if prefix: 457 param = '{0}{1}'.format(prefix, param) 458 459 from_msg = '{0}: {1!r}'.format(param, value) 460 to_msg = '{0}: {1!r}'.format(param, to_text(value)) 461 462 if self._string_conversion_action == 'error': 463 msg = common_msg.capitalize() 464 raise TypeError(to_native(msg)) 465 elif self._string_conversion_action == 'warn': 466 msg = ('The value "{0}" (type {1.__class__.__name__}) was converted to "{2}" (type string). ' 467 'If this does not look like what you expect, {3}').format(from_msg, value, to_msg, common_msg) 468 self.warn(to_native(msg)) 469 return to_native(value, errors='surrogate_or_strict') 470 471 def _handle_options(self, argument_spec=None, params=None, prefix=''): 472 ''' deal with options to create sub spec ''' 473 if argument_spec is None: 474 argument_spec = self.argument_spec 475 if params is None: 476 params = self.params 477 478 for (k, v) in argument_spec.items(): 479 wanted = v.get('type', None) 480 if wanted == 'dict' or (wanted == 'list' and v.get('elements', '') == 'dict'): 481 spec = v.get('options', None) 482 if v.get('apply_defaults', False): 483 if spec is not None: 484 if params.get(k) is None: 485 params[k] = {} 486 else: 487 continue 488 elif spec is None or k not in params or params[k] is None: 489 continue 490 491 self._options_context.append(k) 492 493 if isinstance(params[k], dict): 494 elements = [params[k]] 495 else: 496 elements = params[k] 497 498 for idx, param in enumerate(elements): 499 if not isinstance(param, dict): 500 self.fail_json(msg="value of %s must be of type dict or list of dict" % k) 501 502 new_prefix = prefix + k 503 if wanted == 'list': 504 new_prefix += '[%d]' % idx 505 new_prefix += '.' 506 507 self._set_fallbacks(spec, param) 508 options_aliases = self._handle_aliases(spec, param, option_prefix=new_prefix) 509 510 options_legal_inputs = list(spec.keys()) + list(options_aliases.keys()) 511 512 self._check_arguments(spec, param, options_legal_inputs) 513 514 # check exclusive early 515 if not self.bypass_checks: 516 self._check_mutually_exclusive(v.get('mutually_exclusive', None), param) 517 518 self._set_defaults(pre=True, spec=spec, param=param) 519 520 if not self.bypass_checks: 521 self._check_required_arguments(spec, param) 522 self._check_argument_types(spec, param, new_prefix) 523 self._check_argument_values(spec, param) 524 525 self._check_required_together(v.get('required_together', None), param) 526 self._check_required_one_of(v.get('required_one_of', None), param) 527 self._check_required_if(v.get('required_if', None), param) 528 self._check_required_by(v.get('required_by', None), param) 529 530 self._set_defaults(pre=False, spec=spec, param=param) 531 532 # handle multi level options (sub argspec) 533 self._handle_options(spec, param, new_prefix) 534 self._options_context.pop() 535 536 def _get_wanted_type(self, wanted, k): 537 if not callable(wanted): 538 if wanted is None: 539 # Mostly we want to default to str. 540 # For values set to None explicitly, return None instead as 541 # that allows a user to unset a parameter 542 wanted = 'str' 543 try: 544 type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted] 545 except KeyError: 546 self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k)) 547 else: 548 # set the type_checker to the callable, and reset wanted to the callable's name (or type if it doesn't have one, ala MagicMock) 549 type_checker = wanted 550 wanted = getattr(wanted, '__name__', to_native(type(wanted))) 551 552 return type_checker, wanted 553 554 def _handle_elements(self, wanted, param, values): 555 type_checker, wanted_name = self._get_wanted_type(wanted, param) 556 validated_params = [] 557 # Get param name for strings so we can later display this value in a useful error message if needed 558 # Only pass 'kwargs' to our checkers and ignore custom callable checkers 559 kwargs = {} 560 if wanted_name == 'str' and isinstance(wanted, string_types): 561 if isinstance(param, string_types): 562 kwargs['param'] = param 563 elif isinstance(param, dict): 564 kwargs['param'] = list(param.keys())[0] 565 for value in values: 566 try: 567 validated_params.append(type_checker(value, **kwargs)) 568 except (TypeError, ValueError) as e: 569 msg = "Elements value for option %s" % param 570 if self._options_context: 571 msg += " found in '%s'" % " -> ".join(self._options_context) 572 msg += " is of type %s and we were unable to convert to %s: %s" % (type(value), wanted_name, to_native(e)) 573 self.fail_json(msg=msg) 574 return validated_params 575 576 def _check_argument_types(self, spec=None, param=None, prefix=''): 577 ''' ensure all arguments have the requested type ''' 578 579 if spec is None: 580 spec = self.argument_spec 581 if param is None: 582 param = self.params 583 584 for (k, v) in spec.items(): 585 wanted = v.get('type', None) 586 if k not in param: 587 continue 588 589 value = param[k] 590 if value is None: 591 continue 592 593 type_checker, wanted_name = self._get_wanted_type(wanted, k) 594 # Get param name for strings so we can later display this value in a useful error message if needed 595 # Only pass 'kwargs' to our checkers and ignore custom callable checkers 596 kwargs = {} 597 if wanted_name == 'str' and isinstance(type_checker, string_types): 598 kwargs['param'] = list(param.keys())[0] 599 600 # Get the name of the parent key if this is a nested option 601 if prefix: 602 kwargs['prefix'] = prefix 603 604 try: 605 param[k] = type_checker(value, **kwargs) 606 wanted_elements = v.get('elements', None) 607 if wanted_elements: 608 if wanted != 'list' or not isinstance(param[k], list): 609 msg = "Invalid type %s for option '%s'" % (wanted_name, param) 610 if self._options_context: 611 msg += " found in '%s'." % " -> ".join(self._options_context) 612 msg += ", elements value check is supported only with 'list' type" 613 self.fail_json(msg=msg) 614 param[k] = self._handle_elements(wanted_elements, k, param[k]) 615 616 except (TypeError, ValueError) as e: 617 msg = "argument %s is of type %s" % (k, type(value)) 618 if self._options_context: 619 msg += " found in '%s'." % " -> ".join(self._options_context) 620 msg += " and we were unable to convert to %s: %s" % (wanted_name, to_native(e)) 621 self.fail_json(msg=msg) 622 623 def _set_defaults(self, pre=True, spec=None, param=None): 624 if spec is None: 625 spec = self.argument_spec 626 if param is None: 627 param = self.params 628 for (k, v) in spec.items(): 629 default = v.get('default', None) 630 if pre is True: 631 # this prevents setting defaults on required items 632 if default is not None and k not in param: 633 param[k] = default 634 else: 635 # make sure things without a default still get set None 636 if k not in param: 637 param[k] = default 638 639 def _set_fallbacks(self, spec=None, param=None): 640 if spec is None: 641 spec = self.argument_spec 642 if param is None: 643 param = self.params 644 645 for (k, v) in spec.items(): 646 fallback = v.get('fallback', (None,)) 647 fallback_strategy = fallback[0] 648 fallback_args = [] 649 fallback_kwargs = {} 650 if k not in param and fallback_strategy is not None: 651 for item in fallback[1:]: 652 if isinstance(item, dict): 653 fallback_kwargs = item 654 else: 655 fallback_args = item 656 try: 657 param[k] = fallback_strategy(*fallback_args, **fallback_kwargs) 658 except AnsibleFallbackNotFound: 659 continue 660 661 def warn(self, warning): 662 # Copied from ansible.module_utils.common.warnings: 663 if isinstance(warning, string_types): 664 self.__warnings.append(warning) 665 else: 666 raise TypeError("warn requires a string not a %s" % type(warning)) 667 668 def deprecate(self, msg, version=None, date=None, collection_name=None): 669 if version is not None and date is not None: 670 raise AssertionError("implementation error -- version and date must not both be set") 671 672 # Copied from ansible.module_utils.common.warnings: 673 if isinstance(msg, string_types): 674 # For compatibility, we accept that neither version nor date is set, 675 # and treat that the same as if version would haven been set 676 if date is not None: 677 self.__deprecations.append({'msg': msg, 'date': date, 'collection_name': collection_name}) 678 else: 679 self.__deprecations.append({'msg': msg, 'version': version, 'collection_name': collection_name}) 680 else: 681 raise TypeError("deprecate requires a string not a %s" % type(msg)) 682 683 def _return_formatted(self, kwargs): 684 if 'invocation' not in kwargs: 685 kwargs['invocation'] = {'module_args': self.params} 686 687 if 'warnings' in kwargs: 688 if isinstance(kwargs['warnings'], list): 689 for w in kwargs['warnings']: 690 self.warn(w) 691 else: 692 self.warn(kwargs['warnings']) 693 694 if self.__warnings: 695 kwargs['warnings'] = self.__warnings 696 697 if 'deprecations' in kwargs: 698 if isinstance(kwargs['deprecations'], list): 699 for d in kwargs['deprecations']: 700 if isinstance(d, SEQUENCETYPE) and len(d) == 2: 701 self.deprecate(d[0], version=d[1]) 702 elif isinstance(d, Mapping): 703 self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'), 704 collection_name=d.get('collection_name')) 705 else: 706 self.deprecate(d) # pylint: disable=ansible-deprecated-no-version 707 else: 708 self.deprecate(kwargs['deprecations']) # pylint: disable=ansible-deprecated-no-version 709 710 if self.__deprecations: 711 kwargs['deprecations'] = self.__deprecations 712 713 kwargs = remove_values(kwargs, self.no_log_values) 714 raise _ModuleExitException(kwargs) 715 716 def exit_json(self, **kwargs): 717 result = dict(kwargs) 718 if 'failed' not in result: 719 result['failed'] = False 720 self._return_formatted(result) 721 722 def fail_json(self, msg, **kwargs): 723 result = dict(kwargs) 724 result['failed'] = True 725 result['msg'] = msg 726 self._return_formatted(result) 727 728 729@six.add_metaclass(abc.ABCMeta) 730class ActionModuleBase(ActionBase): 731 @abc.abstractmethod 732 def setup_module(self): 733 """Return pair (ArgumentSpec, kwargs).""" 734 pass 735 736 @abc.abstractmethod 737 def run_module(self, module): 738 """Run module code""" 739 module.fail_json(msg='Not implemented.') 740 741 def run(self, tmp=None, task_vars=None): 742 if task_vars is None: 743 task_vars = dict() 744 745 result = super(ActionModuleBase, self).run(tmp, task_vars) 746 del tmp # tmp no longer has any effect 747 748 try: 749 argument_spec, kwargs = self.setup_module() 750 module = argument_spec.create_ansible_module_helper(AnsibleActionModule, (self, ), **kwargs) 751 self.run_module(module) 752 raise AnsibleError('Internal error: action module did not call module.exit_json()') 753 except _ModuleExitException as mee: 754 result.update(mee.result) 755 return result 756 except Exception as dummy: 757 result['failed'] = True 758 result['msg'] = 'MODULE FAILURE' 759 result['exception'] = traceback.format_exc() 760 return result 761 762 763class ArgumentSpec: 764 def __init__(self, argument_spec, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None): 765 self.argument_spec = argument_spec 766 self.mutually_exclusive = mutually_exclusive or [] 767 self.required_together = required_together or [] 768 self.required_one_of = required_one_of or [] 769 self.required_if = required_if or [] 770 self.required_by = required_by or {} 771 772 def create_ansible_module_helper(self, clazz, args, **kwargs): 773 return clazz( 774 *args, 775 argument_spec=self.argument_spec, 776 mutually_exclusive=self.mutually_exclusive, 777 required_together=self.required_together, 778 required_one_of=self.required_one_of, 779 required_if=self.required_if, 780 required_by=self.required_by, 781 **kwargs) 782