From 0624951e177466912f4bda642b72dd2d9c54c1cd Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Thu, 17 Nov 2022 18:55:00 +1300 Subject: [PATCH] add dependency manager (#5535) * add dependency manager * add plugins/module_utils/deps.py to BOTMETA * ditch usng OrderedDict to keep compatibility with Python 2.6 * Update plugins/module_utils/deps.py Co-authored-by: Felix Fontein Co-authored-by: Felix Fontein --- .github/BOTMETA.yml | 2 + plugins/module_utils/deps.py | 90 +++++++++++++++++++++++++++++++ plugins/modules/dnsimple_info.py | 16 ++---- plugins/modules/iso_customize.py | 19 +++---- plugins/modules/pagerduty_user.py | 27 ++-------- plugins/modules/pids.py | 15 +++--- plugins/modules/snmp_facts.py | 17 ++---- 7 files changed, 117 insertions(+), 69 deletions(-) create mode 100644 plugins/module_utils/deps.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index c56720cb0d..2fbbf64f65 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -265,6 +265,8 @@ files: maintainers: delineaKrehl tylerezimmerman $module_utils/: labels: module_utils + $module_utils/deps.py: + maintainers: russoz $module_utils/gconftool2.py: labels: gconftool2 maintainers: russoz diff --git a/plugins/module_utils/deps.py b/plugins/module_utils/deps.py new file mode 100644 index 0000000000..bfb94cbc09 --- /dev/null +++ b/plugins/module_utils/deps.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# (c) 2022, Alexei Znamensky +# Copyright (c) 2022, Ansible Project +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import traceback +from contextlib import contextmanager + +from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.basic import missing_required_lib + + +_deps = dict() + + +class _Dependency(object): + _states = ["pending", "failure", "success"] + + def __init__(self, name, reason=None, url=None, msg=None): + self.name = name + self.reason = reason + self.url = url + self.msg = msg + + self.state = 0 + self.trace = None + self.exc = None + + def succeed(self): + self.state = 2 + + def fail(self, exc, trace): + self.state = 1 + self.exc = exc + self.trace = trace + + @property + def message(self): + if self.msg: + return to_native(self.msg) + else: + return missing_required_lib(self.name, reason=self.reason, url=self.url) + + @property + def failed(self): + return self.state == 1 + + def verify(self, module): + if self.failed: + module.fail_json(msg=self.message, exception=self.trace) + + def __str__(self): + return "".format(self.name, self._states[self.state]) + + +@contextmanager +def declare(name, *args, **kwargs): + dep = _Dependency(name, *args, **kwargs) + try: + yield dep + except Exception as e: + dep.fail(e, traceback.format_exc()) + else: + dep.succeed() + finally: + _deps[name] = dep + + +def validate(module, spec=None): + dep_names = sorted(_deps) + + if spec is not None: + if spec.startswith("-"): + spec_split = spec[1:].split(":") + for d in spec_split: + dep_names.remove(d) + else: + spec_split = spec[1:].split(":") + dep_names = [] + for d in spec_split: + _deps[d] # ensure it exists + dep_names.append(d) + + for dep in dep_names: + _deps[dep].verify(module) diff --git a/plugins/modules/dnsimple_info.py b/plugins/modules/dnsimple_info.py index 959bacbbe8..52fd53303f 100644 --- a/plugins/modules/dnsimple_info.py +++ b/plugins/modules/dnsimple_info.py @@ -230,18 +230,11 @@ dnsimple_record_info: type: str ''' -import traceback from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.basic import missing_required_lib +from ansible_collections.community.general.plugins.module_utils import deps -try: +with deps.declare("requests"): from requests import Request, Session -except ImportError: - HAS_REQUESTS = False - REQUESTS_IMPORT_ERROR = traceback.format_exc() -else: - HAS_REQUESTS = True - REQUESTS_IMPORT_ERROR = None def build_url(account, key, is_sandbox): @@ -310,10 +303,7 @@ def main(): params['api_key'], params['sandbox']) - if not HAS_REQUESTS: - module.exit_json( - msg=missing_required_lib('requests'), - exception=REQUESTS_IMPORT_ERROR) + deps.validate(module) # At minimum we need account and key if params['account_id'] and params['api_key']: diff --git a/plugins/modules/iso_customize.py b/plugins/modules/iso_customize.py index c3a2ae2651..4f902f47e4 100644 --- a/plugins/modules/iso_customize.py +++ b/plugins/modules/iso_customize.py @@ -97,19 +97,14 @@ dest_iso: ''' import os -import traceback -PYCDLIB_IMP_ERR = None -try: - import pycdlib - HAS_PYCDLIB = True -except ImportError: - PYCDLIB_IMP_ERR = traceback.format_exc() - HAS_PYCDLIB = False - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.community.general.plugins.module_utils import deps +from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native +with deps.declare("pycdlib"): + import pycdlib + # The upper dir exist, we only add subdirectoy def iso_add_dir(module, opened_iso, iso_type, dir_path): @@ -306,9 +301,7 @@ def main(): required_one_of=[('delete_files', 'add_files'), ], supports_check_mode=True, ) - if not HAS_PYCDLIB: - module.fail_json( - missing_required_lib('pycdlib'), exception=PYCDLIB_IMP_ERR) + deps.validate(module) src_iso = module.params['src_iso'] if not os.path.exists(src_iso): diff --git a/plugins/modules/pagerduty_user.py b/plugins/modules/pagerduty_user.py index 4d8e32248f..e16fe59e76 100644 --- a/plugins/modules/pagerduty_user.py +++ b/plugins/modules/pagerduty_user.py @@ -80,25 +80,12 @@ EXAMPLES = r''' RETURN = r''' # ''' -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -import traceback from os import path +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils import deps -try: - from pdpyras import APISession - HAS_PD_PY = True - PD_IMPORT_ERR = None -except ImportError: - HAS_PD_PY = False - PD_IMPORT_ERR = traceback.format_exc() - -try: - from pdpyras import PDClientError - HAS_PD_CLIENT_ERR = True - PD_CLIENT_ERR_IMPORT_ERR = None -except ImportError: - HAS_PD_CLIENT_ERR = False - PD_CLIENT_ERR_IMPORT_ERR = traceback.format_exc() +with deps.declare("pdpyras", url="https://github.com/PagerDuty/pdpyras"): + from pdpyras import APISession, PDClientError class PagerDutyUser(object): @@ -202,11 +189,7 @@ def main(): supports_check_mode=True, ) - if not HAS_PD_PY: - module.fail_json(msg=missing_required_lib('pdpyras', url='https://github.com/PagerDuty/pdpyras'), exception=PD_IMPORT_ERR) - - if not HAS_PD_CLIENT_ERR: - module.fail_json(msg=missing_required_lib('PDClientError', url='https://github.com/PagerDuty/pdpyras'), exception=PD_CLIENT_ERR_IMPORT_ERR) + deps.validate(module) access_token = module.params['access_token'] pd_user = module.params['pd_user'] diff --git a/plugins/modules/pids.py b/plugins/modules/pids.py index eaaf7f9437..2fe2a6b8ac 100644 --- a/plugins/modules/pids.py +++ b/plugins/modules/pids.py @@ -3,8 +3,8 @@ # Copyright (c) 2019, Saranya Sridharan # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later -from __future__ import (absolute_import, division, print_function) +from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' @@ -60,18 +60,15 @@ import re from os.path import basename from ansible.module_utils import six -from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils import deps from ansible.module_utils.common.text.converters import to_native from ansible_collections.community.general.plugins.module_utils.version import LooseVersion -try: +with deps.declare("psutil"): import psutil - HAS_PSUTIL = True -except ImportError: - HAS_PSUTIL = False - class PSAdapterError(Exception): pass @@ -177,8 +174,8 @@ def compare_lower(a, b): class Pids(object): def __init__(self, module): - if not HAS_PSUTIL: - module.fail_json(msg=missing_required_lib('psutil')) + + deps.validate(module) self._ps = PSAdapter.from_package(psutil) diff --git a/plugins/modules/snmp_facts.py b/plugins/modules/snmp_facts.py index 71821faaa8..0242bc6dde 100644 --- a/plugins/modules/snmp_facts.py +++ b/plugins/modules/snmp_facts.py @@ -183,20 +183,14 @@ ansible_interfaces: ''' import binascii -import traceback from collections import defaultdict +from ansible_collections.community.general.plugins.module_utils import deps +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text -PYSNMP_IMP_ERR = None -try: +with deps.declare("pysnmp"): from pysnmp.entity.rfc3413.oneliner import cmdgen from pysnmp.proto.rfc1905 import EndOfMibView - HAS_PYSNMP = True -except Exception: - PYSNMP_IMP_ERR = traceback.format_exc() - HAS_PYSNMP = False - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.common.text.converters import to_text class DefineOid(object): @@ -299,8 +293,7 @@ def main(): m_args = module.params - if not HAS_PYSNMP: - module.fail_json(msg=missing_required_lib('pysnmp'), exception=PYSNMP_IMP_ERR) + deps.validate(module) cmdGen = cmdgen.CommandGenerator() transport_opts = dict((k, m_args[k]) for k in ('timeout', 'retries') if m_args[k] is not None)