Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable access to available plugins #277

Merged
merged 2 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,4 @@ dmypy.json
.pyre/
.test-results
*.lcov
ansible_collections
4 changes: 1 addition & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
"[python]": {
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.ruff": true,
"source.organizeImports": false,
"source.organizeImports.ruff": true
"source.organizeImports": false
}
},
"editor.formatOnSave": true,
Expand Down
3 changes: 3 additions & 0 deletions ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[defaults]
# isolate testing of ansible-compat from user local setup
collections_path = .
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
72 changes: 70 additions & 2 deletions src/ansible_compat/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@
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

from ansible_compat.config import (
AnsibleConfig,
ansible_collections_path,
ansible_version,
parse_ansible_version,
)
from ansible_compat.constants import (
Expand Down Expand Up @@ -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."""
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions test/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
11 changes: 6 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -140,6 +142,5 @@ deps =
description = Build docs
commands =
mkdocs {posargs:build} --strict
deps =
--editable .[docs]
extras = docs
passenv = *