From 31cdff7d47b39ba3cbde6051ca6caf3e3aa8eeb3 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 + .vscode/settings.json | 2 - ansible.cfg | 3 ++ pyproject.toml | 1 + src/ansible_compat/runtime.py | 72 ++++++++++++++++++++++++++++++++++- test/test_runtime.py | 42 ++++++++++++++++++++ tox.ini | 11 +++--- 7 files changed, 123 insertions(+), 9 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/.vscode/settings.json b/.vscode/settings.json index 061f6b9f..e156909a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,9 +5,7 @@ "[python]": { "editor.codeActionsOnSave": { "source.fixAll": true, - "source.fixAll.ruff": true, "source.organizeImports": false, - "source.organizeImports.ruff": true } }, "editor.formatOnSave": true, 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/pyproject.toml b/pyproject.toml index f5abbc3a..cc1149d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,7 @@ disable = [ "import-error", # already covered by ruff which is faster "too-many-arguments", # PLR0913 + "raise-missing-from", # Temporary disable duplicate detection we remove old code from prerun "duplicate-code", ] diff --git a/src/ansible_compat/runtime.py b/src/ansible_compat/runtime.py index bbb49c2e..841c10cd 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 @@ -20,6 +20,7 @@ from ansible_compat.config import ( AnsibleConfig, ansible_collections_path, + ansible_version, parse_ansible_version, ) from ansible_compat.constants import ( @@ -73,6 +74,71 @@ 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: + if ansible_version() < Version("2.14") and attr in {"filter", "test"}: + msg = "Ansible version below 2.14 does not support retrieving filter and test plugins." + raise RuntimeError(msg) from exc + proc = self.runtime.run( + ["ansible-doc", "--json", "-l", "-t", attr], + ) + data = json.loads(proc.stdout) + if not isinstance(data, dict): # pragma: no cover + 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 +149,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 +186,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..b09fd779 100644 --- a/test/test_runtime.py +++ b/test/test_runtime.py @@ -15,6 +15,7 @@ from packaging.version import Version from pytest_mock import MockerFixture +from ansible_compat.config import ansible_version from ansible_compat.constants import INVALID_PREREQUISITES_RC from ansible_compat.errors import ( AnsibleCommandError, @@ -747,3 +748,44 @@ 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 len(runtime.plugins.cliconf) == 0 + # ansible.netcommon.restconf might be in httpapi + assert isinstance(runtime.plugins.httpapi, dict) + # "ansible.netcommon.default" might be in runtime.plugins.netconf + assert isinstance(runtime.plugins.netconf, dict) + assert isinstance(runtime.plugins.role, dict) + assert "become" in runtime.plugins.keyword + + if ansible_version() < Version("2.14.0"): + assert "sudo" in runtime.plugins.become + assert "memory" in runtime.plugins.cache + assert "default" in runtime.plugins.callback + assert "local" in runtime.plugins.connection + assert "ini" in runtime.plugins.inventory + assert "env" in runtime.plugins.lookup + assert "sh" in runtime.plugins.shell + assert "host_group_vars" in runtime.plugins.vars + assert "file" in runtime.plugins.module + assert "free" in runtime.plugins.strategy + # ansible-doc below 2.14 does not support listing 'test' and 'filter' types: + with pytest.raises(RuntimeError): + assert "is_abs" in runtime.plugins.test + with pytest.raises(RuntimeError): + assert "bool" in runtime.plugins.filter + else: + 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 "ansible.builtin.local" in runtime.plugins.connection + assert "ansible.builtin.ini" in runtime.plugins.inventory + assert "ansible.builtin.env" in runtime.plugins.lookup + 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 diff --git a/tox.ini b/tox.ini index 1f4416bf..b5ae7dac 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,6 @@ envlist = py{39,310,311}{,-devel,-ansible212,-ansible213,-ansible214,-ansible215} isolated_build = true skip_missing_interpreters = True -skipsdist = true [testenv] description = @@ -28,7 +27,8 @@ deps = devel: ansible-core @ git+https://github.com/ansible/ansible.git # GPLv3+ # avoid installing ansible-core on -devel envs: !devel: ansible-core - --editable .[test] +extras = + test commands = sh -c "ansible --version | head -n 1" @@ -68,12 +68,14 @@ 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 git sh +# https://tox.wiki/en/latest/upgrading.html#editable-mode +package = editable [testenv:lint] description = Run all linters @@ -140,6 +142,5 @@ deps = description = Build docs commands = mkdocs {posargs:build} --strict -deps = - --editable .[docs] +extras = docs passenv = *