From 4f2b8bbe89aee81f1bb20ee00914367f8c6703c8 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Tue, 23 May 2023 11:30:48 +0100 Subject: [PATCH] Enable access to available plugins Related: https://github.com/ansible/ansible-lint/issues/3481 --- .gitignore | 1 + ansible.cfg | 3 ++ src/ansible_compat/runtime.py | 68 +++++++++++++++++++++++++++++++++-- test/test_runtime.py | 23 ++++++++++++ tox.ini | 2 +- 5 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 ansible.cfg diff --git a/.gitignore b/.gitignore index 2cfccf56..0d80231b 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,4 @@ dmypy.json .pyre/ .test-results *.lcov +ansible_collections diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 00000000..c92c9cea --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +# isolate testing of ansible-compat from user local setup +collections_path = . diff --git a/src/ansible_compat/runtime.py b/src/ansible_compat/runtime.py index bbb49c2e..9656a87e 100644 --- a/src/ansible_compat/runtime.py +++ b/src/ansible_compat/runtime.py @@ -10,9 +10,9 @@ import tempfile import warnings from collections import OrderedDict -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Optional, Union, no_type_check import subprocess_tee from packaging.version import Version @@ -73,6 +73,68 @@ def __init__(self, version: str) -> None: super().__init__(version) +@dataclass +class Plugins: # pylint: disable=too-many-instance-attributes + """Dataclass to access installed Ansible plugins, uses ansible-doc to retrieve them.""" + + runtime: "Runtime" + become: dict[str, str] = field(init=False) + cache: dict[str, str] = field(init=False) + callback: dict[str, str] = field(init=False) + cliconf: dict[str, str] = field(init=False) + connection: dict[str, str] = field(init=False) + httpapi: dict[str, str] = field(init=False) + inventory: dict[str, str] = field(init=False) + lookup: dict[str, str] = field(init=False) + netconf: dict[str, str] = field(init=False) + shell: dict[str, str] = field(init=False) + vars: dict[str, str] = field(init=False) # noqa: A003 + module: dict[str, str] = field(init=False) + strategy: dict[str, str] = field(init=False) + test: dict[str, str] = field(init=False) + filter: dict[str, str] = field(init=False) # noqa: A003 + role: dict[str, str] = field(init=False) + keyword: dict[str, str] = field(init=False) + + @no_type_check + def __getattribute__(self, attr: str): # noqa: ANN204 + """Get attribute.""" + if attr in { + "become", + "cache", + "callback", + "cliconf", + "connection", + "httpapi", + "inventory", + "lookup", + "netconf", + "shell", + "vars", + "module", + "strategy", + "test", + "filter", + "role", + "keyword", + }: + try: + result = super().__getattribute__(attr) + except AttributeError as exc: + proc = self.runtime.run( + ["ansible-doc", "--json", "-l", "-t", attr], + ) + data = json.loads(proc.stdout) + if not isinstance(data, dict): + msg = "Unexpected output from ansible-doc" + raise AnsibleCompatError(msg) from exc + result = data + else: + result = super().__getattribute__(attr) + + return result + + # pylint: disable=too-many-instance-attributes class Runtime: """Ansible Runtime manager.""" @@ -83,6 +145,7 @@ class Runtime: # Used to track if we have already initialized the Ansible runtime as attempts # to do it multiple tilmes will cause runtime warnings from within ansible-core initialized: bool = False + plugins: Plugins def __init__( self, @@ -119,6 +182,7 @@ def __init__( self.isolated = isolated self.max_retries = max_retries self.environ = environ or os.environ.copy() + self.plugins = Plugins(runtime=self) # Reduce noise from paramiko, unless user already defined PYTHONWARNINGS # paramiko/transport.py:236: CryptographyDeprecationWarning: Blowfish has been deprecated # https://github.com/paramiko/paramiko/issues/2038 diff --git a/test/test_runtime.py b/test/test_runtime.py index 4759f21e..b9ee2619 100644 --- a/test/test_runtime.py +++ b/test/test_runtime.py @@ -747,3 +747,26 @@ def test_runtime_exec_env(runtime: Runtime) -> None: runtime.environ["FOO"] = "bar" result = runtime.run(["printenv", "FOO"]) assert result.stdout.rstrip() == "bar" + + +def test_runtime_plugins(runtime: Runtime) -> None: + """Tests ability to access detected plugins.""" + assert "ansible.builtin.sudo" in runtime.plugins.become + assert "ansible.builtin.memory" in runtime.plugins.cache + assert "ansible.builtin.default" in runtime.plugins.callback + assert len(runtime.plugins.cliconf) == 0 + assert "ansible.builtin.local" in runtime.plugins.connection + # ansible.netcommon.restconf might be in httpapi + assert isinstance(runtime.plugins.httpapi, dict) + assert "ansible.builtin.ini" in runtime.plugins.inventory + assert "ansible.builtin.env" in runtime.plugins.lookup + # "ansible.netcommon.default" might be in runtime.plugins.netconf + assert isinstance(runtime.plugins.netconf, dict) + assert "ansible.builtin.sh" in runtime.plugins.shell + assert "ansible.builtin.host_group_vars" in runtime.plugins.vars + assert "ansible.builtin.file" in runtime.plugins.module + assert "ansible.builtin.free" in runtime.plugins.strategy + assert "ansible.builtin.is_abs" in runtime.plugins.test + assert "ansible.builtin.bool" in runtime.plugins.filter + assert isinstance(runtime.plugins.role, dict) + assert "become" in runtime.plugins.keyword diff --git a/tox.ini b/tox.ini index 1f4416bf..75f779c6 100644 --- a/tox.ini +++ b/tox.ini @@ -68,7 +68,7 @@ setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 PIP_CONSTRAINT = {toxinidir}/requirements.txt PRE_COMMIT_COLOR = always - PYTEST_REQPASS = 81 + PYTEST_REQPASS = 82 FORCE_COLOR = 1 allowlist_externals = ansible