1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Modhelper improvements (#1480)

* Improvements in module_helper

- added `ModuleHelperException` to handle problems specific to ModuleHelper
- updated `module_fails_on_exception` for `ModuleHelperException`
- `StateMixin`: composed names of state methods are now calculated instead of fixed.
- `CmdMixin`: added `run_command_fixed_options` to pass some parameters on every call

* Improvements in module_helper

- Named deprecations: ability to declare a `dict` of deprecations indexed by names, allowing module maintainer to trigger them by those names, and also allowing the module user to acknowledge them in a similar way.
  - Adding `ack_named_deprecations` to module's `argument_spec` when they exist.
  - Providing doc fragment for `ack_named_deprecations`.
- Added method `__quit_module__` providing a hook for code that needs to be run when quitting the module.
- Created convenience classes combining `ModuleHelper`, `StateMixin`, `CmdMixin`.

* fixed validation

* fixed validation

* changelog fragment

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* Improvement on Named Deprecations

Per the comments in PR, we want to expose a call to a ``deprecate`` method on the module code, so that pylint can properly perform its static analysis on deprecations.

This prompted a revamp of the named deprecation feature.

* Use .get() instead of [] for the param to ack named deprecations.

* Changes from suggestions in the PR

* removed named deprecations

* Update changelogs/fragments/1480-module-helper-improvements.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/module_utils/module_helper.py

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Alexei Znamensky 2021-01-16 09:29:23 +13:00 committed by GitHub
parent 1d0c5e2ba4
commit 3e9a6acff7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 28 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- module_helper module utils - multiple convenience features added (https://github.com/ansible-collections/community.general/pull/1480).

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# (c) 2020, Alexei Znamensky <russoz@gmail.com>
# Copyright: (c) 2020, Ansible Project
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import absolute_import, division, print_function
@ -11,6 +12,21 @@ import traceback
from ansible.module_utils.basic import AnsibleModule
class ModuleHelperException(Exception):
@staticmethod
def _get_remove(key, kwargs):
if key in kwargs:
result = kwargs[key]
del kwargs[key]
return result
return None
def __init__(self, *args, **kwargs):
self.msg = self._get_remove('msg', kwargs) or "Module failed with exception: {0}".format(self)
self.update_output = self._get_remove('update_output', kwargs) or {}
super(ModuleHelperException, self).__init__(*args, **kwargs)
class ArgFormat(object):
"""
Argument formatter
@ -102,6 +118,9 @@ def module_fails_on_exception(func):
func(self, *args, **kwargs)
except SystemExit:
raise
except ModuleHelperException as e:
if e.update_output:
self.update_output(e.update_output)
except Exception as e:
self.vars.msg = "Module failed with exception: {0}".format(str(e).strip())
self.vars.exception = traceback.format_exc()
@ -151,16 +170,14 @@ class ModuleHelper(object):
if module:
self.module = module
if not isinstance(module, AnsibleModule):
if isinstance(self.module, dict):
self.module = AnsibleModule(**self.module)
def update_output(self, **kwargs):
if kwargs:
self.output_dict.update(kwargs)
self.output_dict.update(kwargs)
def update_facts(self, **kwargs):
if kwargs:
self.facts_dict.update(kwargs)
self.facts_dict.update(kwargs)
def __init_module__(self):
pass
@ -168,6 +185,9 @@ class ModuleHelper(object):
def __run__(self):
raise NotImplementedError()
def __quit_module__(self):
pass
@property
def changed(self):
return self._changed
@ -189,6 +209,7 @@ class ModuleHelper(object):
self.fail_on_missing_deps()
self.__init_module__()
self.__run__()
self.__quit_module__()
self.module.exit_json(changed=self.changed, **self.output_dict)
@classmethod
@ -213,6 +234,9 @@ class StateMixin(object):
state = self.module.params.get(self.state_param)
return self.default_state if state is None else state
def _method(self, state):
return "{0}_{1}".format(self.state_param, state)
def __run__(self):
state = self._state()
self.vars.state = state
@ -224,14 +248,14 @@ class StateMixin(object):
state = aliased[0]
self.vars.effective_state = state
method = "state_{0}".format(state)
method = self._method(state)
if not hasattr(self, method):
return self.__state_fallback__()
func = getattr(self, method)
return func()
def __state_fallback__(self):
raise ValueError("Cannot find method for state: {0}".format(self._state()))
raise ValueError("Cannot find method: {0}".format(self._method(self._state())))
class CmdMixin(object):
@ -239,7 +263,8 @@ class CmdMixin(object):
Mixin for mapping module options to running a CLI command with its arguments.
"""
command = None
command_args_formats = dict()
command_args_formats = {}
run_command_fixed_options = {}
check_rc = False
force_lang = "C"
@ -266,7 +291,8 @@ class CmdMixin(object):
return self.custom_formats.get(_param, self.module_formats.get(_param))
extra_params = extra_params or dict()
cmd_args = [self.module.get_bin_path(self.command)]
cmd_args = list([self.command]) if isinstance(self.command, str) else list(self.command)
cmd_args[0] = self.module.get_bin_path(cmd_args[0])
param_list = params if params else self.module.params.keys()
for param in param_list:
@ -290,13 +316,26 @@ class CmdMixin(object):
def run_command(self, extra_params=None, params=None, *args, **kwargs):
self.vars['cmd_args'] = self._calculate_args(extra_params, params)
env_update = kwargs.get('environ_update', {})
check_rc = kwargs.get('check_rc', self.check_rc)
options = dict(self.run_command_fixed_options)
env_update = dict(options.get('environ_update', {}))
options['check_rc'] = options.get('check_rc', self.check_rc)
if self.force_lang:
env_update.update({'LANGUAGE': self.force_lang})
self.update_output(force_lang=self.force_lang)
rc, out, err = self.module.run_command(self.vars['cmd_args'],
environ_update=env_update,
check_rc=check_rc, *args, **kwargs)
options['environ_update'] = env_update
options.update(kwargs)
rc, out, err = self.module.run_command(self.vars['cmd_args'], *args, **options)
self.update_output(rc=rc, stdout=out, stderr=err)
return self.process_command_output(rc, out, err)
class StateModuleHelper(StateMixin, ModuleHelper):
pass
class CmdModuleHelper(CmdMixin, ModuleHelper):
pass
class CmdStateModuleHelper(CmdMixin, StateMixin, ModuleHelper):
pass

View file

@ -18,19 +18,28 @@ def single_lambda_2star(x, y, z):
ARG_FORMATS = dict(
simple_boolean_true=("--superflag", ArgFormat.BOOLEAN, 0, True, ["--superflag"]),
simple_boolean_false=("--superflag", ArgFormat.BOOLEAN, 0, False, []),
single_printf=("--param=%s", ArgFormat.PRINTF, 0, "potatoes", ["--param=potatoes"]),
single_printf_no_substitution=("--param", ArgFormat.PRINTF, 0, "potatoes", ["--param"]),
multiple_printf=(["--param", "free-%s"], ArgFormat.PRINTF, 0, "potatoes", ["--param", "free-potatoes"]),
single_format=("--param={0}", ArgFormat.FORMAT, 0, "potatoes", ["--param=potatoes"]),
single_format_no_substitution=("--param", ArgFormat.FORMAT, 0, "potatoes", ["--param"]),
multiple_format=(["--param", "free-{0}"], ArgFormat.FORMAT, 0, "potatoes", ["--param", "free-potatoes"]),
single_lambda_0star=((lambda v: ["piggies=[{0},{1},{2}]".format(v[0], v[1], v[2])]),
None, 0, ['a', 'b', 'c'], ["piggies=[a,b,c]"]),
single_lambda_1star=((lambda a, b, c: ["piggies=[{0},{1},{2}]".format(a, b, c)]),
None, 1, ['a', 'b', 'c'], ["piggies=[a,b,c]"]),
single_lambda_2star=(single_lambda_2star, None, 2, dict(z='c', x='a', y='b'), ["piggies=[a,b,c]"])
simple_boolean_true=("--superflag", ArgFormat.BOOLEAN, 0,
True, ["--superflag"]),
simple_boolean_false=("--superflag", ArgFormat.BOOLEAN, 0,
False, []),
single_printf=("--param=%s", ArgFormat.PRINTF, 0,
"potatoes", ["--param=potatoes"]),
single_printf_no_substitution=("--param", ArgFormat.PRINTF, 0,
"potatoes", ["--param"]),
multiple_printf=(["--param", "free-%s"], ArgFormat.PRINTF, 0,
"potatoes", ["--param", "free-potatoes"]),
single_format=("--param={0}", ArgFormat.FORMAT, 0,
"potatoes", ["--param=potatoes"]),
single_format_no_substitution=("--param", ArgFormat.FORMAT, 0,
"potatoes", ["--param"]),
multiple_format=(["--param", "free-{0}"], ArgFormat.FORMAT, 0,
"potatoes", ["--param", "free-potatoes"]),
single_lambda_0star=((lambda v: ["piggies=[{0},{1},{2}]".format(v[0], v[1], v[2])]), None, 0,
['a', 'b', 'c'], ["piggies=[a,b,c]"]),
single_lambda_1star=((lambda a, b, c: ["piggies=[{0},{1},{2}]".format(a, b, c)]), None, 1,
['a', 'b', 'c'], ["piggies=[a,b,c]"]),
single_lambda_2star=(single_lambda_2star, None, 2,
dict(z='c', x='a', y='b'), ["piggies=[a,b,c]"])
)
ARG_FORMATS_IDS = sorted(ARG_FORMATS.keys())