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