diff --git a/plugins/action/query_graphql.py b/plugins/action/query_graphql.py index 1908135f..2e5dc944 100644 --- a/plugins/action/query_graphql.py +++ b/plugins/action/query_graphql.py @@ -28,6 +28,7 @@ from ansible_collections.networktocode.nautobot.plugins.module_utils.utils import ( NautobotApiBase, NautobotGraphQL, + is_truthy, ) from ansible.utils.display import Display @@ -46,7 +47,12 @@ def nautobot_action_graphql(args): token = args.get("token") or os.getenv("NAUTOBOT_TOKEN") api_version = args.get("api_version") - ssl_verify = args.get("validate_certs", True) + if args.get("validate_certs") is not None: + ssl_verify = args.get("validate_certs") + elif os.getenv("NAUTOBOT_VALIDATE_CERTS") is not None: + ssl_verify = is_truthy(os.getenv("NAUTOBOT_VALIDATE_CERTS")) + else: + ssl_verify = True Display().vv("Verify Certificates: %s" % ssl_verify) # Verify SSL Verify is of boolean diff --git a/plugins/doc_fragments/fragments.py b/plugins/doc_fragments/fragments.py index db80ed28..9421865c 100644 --- a/plugins/doc_fragments/fragments.py +++ b/plugins/doc_fragments/fragments.py @@ -15,11 +15,13 @@ class ModuleDocFragment(object): url: description: - "The URL of the Nautobot instance resolvable by the Ansible host (for example: http://nautobot.example.com:8000)" + - "Can be omitted if the E(NAUTOBOT_URL) environment variable is configured." required: true type: str token: description: - "The token created within Nautobot to authorize API access" + - "Can be omitted if the E(NAUTOBOT_TOKEN) environment variable is configured." required: true type: str state: @@ -40,6 +42,7 @@ class ModuleDocFragment(object): validate_certs: description: - "If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates." + - "Can be omitted if the E(NAUTOBOT_VALIDATE_CERTS) environment variable is configured." required: false default: true type: raw diff --git a/plugins/lookup/lookup.py b/plugins/lookup/lookup.py index b62a5ac8..175b1dd5 100644 --- a/plugins/lookup/lookup.py +++ b/plugins/lookup/lookup.py @@ -142,6 +142,9 @@ from ansible.parsing.splitter import parse_kv, split_args from ansible.utils.display import Display from ansible.module_utils.six import raise_from +from ansible_collections.networktocode.nautobot.plugins.module_utils.utils import ( + is_truthy, +) try: import pynautobot @@ -318,8 +321,13 @@ def run(self, terms, variables=None, **kwargs): api_token = kwargs.get("token") or os.getenv("NAUTOBOT_TOKEN") api_endpoint = kwargs.get("api_endpoint") or os.getenv("NAUTOBOT_URL") + if kwargs.get("validate_certs") is not None: + ssl_verify = kwargs.get("validate_certs") + elif os.getenv("NAUTOBOT_VALIDATE_CERTS") is not None: + ssl_verify = is_truthy(os.getenv("NAUTOBOT_VALIDATE_CERTS")) + else: + ssl_verify = True num_retries = kwargs.get("num_retries", "0") - ssl_verify = kwargs.get("validate_certs", True) api_filter = kwargs.get("api_filter") raw_return = kwargs.get("raw_data") plugin = kwargs.get("plugin") diff --git a/plugins/lookup/lookup_graphql.py b/plugins/lookup/lookup_graphql.py index 6e785562..8d4deaf2 100644 --- a/plugins/lookup/lookup_graphql.py +++ b/plugins/lookup/lookup_graphql.py @@ -121,6 +121,7 @@ from ansible_collections.networktocode.nautobot.plugins.module_utils.utils import ( NautobotApiBase, NautobotGraphQL, + is_truthy, ) except ModuleNotFoundError: # For testing @@ -151,7 +152,14 @@ def nautobot_lookup_graphql(**kwargs): raise AnsibleLookupError("Missing URL of Nautobot") token = kwargs.get("token") or os.getenv("NAUTOBOT_TOKEN") - ssl_verify = kwargs.get("validate_certs", True) + + if kwargs.get("validate_certs") is not None: + ssl_verify = kwargs.get("validate_certs") + elif os.getenv("NAUTOBOT_VALIDATE_CERTS") is not None: + ssl_verify = is_truthy(os.getenv("NAUTOBOT_VALIDATE_CERTS")) + else: + ssl_verify = True + api_version = kwargs.get("api_version") Display().vv("Validate certs: %s" % ssl_verify) diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py index d56fc670..3443ba33 100644 --- a/plugins/module_utils/utils.py +++ b/plugins/module_utils/utils.py @@ -420,6 +420,31 @@ ) +def is_truthy(arg): + """ + Convert "truthy" strings into Booleans. + + Examples: + >>> is_truthy('yes') + True + + Args: + arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no, + f, false, off and 0. Raises ValueError if val is anything else. + """ + + if isinstance(arg, bool): + return arg + + val = str(arg).lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError(f"Invalid truthy value: `{arg}`") + + class NautobotModule: """ Initialize connection to Nautobot, sets AnsibleModule passed in to @@ -994,7 +1019,12 @@ class NautobotApiBase: def __init__(self, **kwargs): self.url = kwargs.get("url") or os.getenv("NAUTOBOT_URL") self.token = kwargs.get("token") or os.getenv("NAUTOBOT_TOKEN") - self.ssl_verify = kwargs.get("ssl_verify", True) + if kwargs.get("ssl_verify") is not None: + self.ssl_verify = kwargs.get("ssl_verify") + elif os.getenv("NAUTOBOT_VALIDATE_CERTS") is not None: + self.ssl_verify = is_truthy(os.getenv("NAUTOBOT_VALIDATE_CERTS")) + else: + self.ssl_verify = True self.api_version = kwargs.get("api_version") # Setup the API client calls diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c885002c..446ada3c 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -20,7 +20,7 @@ def patch_pynautobot_version_check(monkeypatch): @pytest.fixture def nautobot_api_base(): - return NautobotApiBase(url="https://nautobot.mock.com", token="abc123", valdiate_certs=False) + return NautobotApiBase(url="https://nautobot.mock.com", token="abc123", validate_certs=False) @pytest.fixture diff --git a/tests/unit/module_utils/test_nautobot_base_class.py b/tests/unit/module_utils/test_nautobot_base_class.py index 47bca429..e71b110c 100644 --- a/tests/unit/module_utils/test_nautobot_base_class.py +++ b/tests/unit/module_utils/test_nautobot_base_class.py @@ -13,10 +13,11 @@ from functools import partial from unittest.mock import patch, MagicMock, Mock from ansible.module_utils.basic import AnsibleModule +from ansible.errors import AnsibleError import pynautobot try: - from ansible_collections.networktocode.nautobot.plugins.module_utils.utils import NautobotModule + from ansible_collections.networktocode.nautobot.plugins.module_utils.utils import NautobotModule, NautobotApiBase from ansible_collections.networktocode.nautobot.plugins.module_utils.dcim import NB_DEVICES from ansible_collections.networktocode.nautobot.tests.test_data import load_test_data @@ -29,7 +30,7 @@ sys.path.append("plugins/module_utils") sys.path.append("tests") - from utils import NautobotModule + from utils import NautobotModule, NautobotApiBase from dcim import NB_DEVICES from test_data import load_test_data @@ -332,3 +333,36 @@ def test_invalid_api_version_error_handling(mock_ansible_module, monkeypatch, ap monkeypatch.setattr(pynautobot.api, "version", api_version) module = NautobotModule(mock_ansible_module, "devices") assert isinstance(module.nb, obj_type) + + +@patch.dict(os.environ, {}) +def test_validate_certs_defaults_true(): + """Test that the default SSL verify is set as true and no environment variable is set.""" + test_class = NautobotApiBase(url="https://nautobot.example.com", token="abc123") + assert os.getenv("NAUTOBOT_VALIDATE_CERTS") is None + assert test_class.ssl_verify is True + + +@patch.dict(os.environ, {"NAUTOBOT_VALIDATE_CERTS": "FALSE"}) +def test_validate_certs_environment_var_false(): + """Test that the default SSL verify is set as false via environment variable.""" + test_class = NautobotApiBase(url="https://nautobot.example.com", token="abc123") + assert os.getenv("NAUTOBOT_VALIDATE_CERTS") is not None + assert test_class.ssl_verify is False + + +@patch.dict(os.environ, {"NAUTOBOT_VALIDATE_CERTS": "FALSE"}) +def test_validate_certs_override(): + """Test that the default SSL verify is set as true via API class, and overrides environment variable.""" + test_class = NautobotApiBase(url="https://nautobot.example.com", token="abc123", ssl_verify=True) + assert os.getenv("NAUTOBOT_VALIDATE_CERTS") is not None + assert os.getenv("NAUTOBOT_VALIDATE_CERTS") == "FALSE" + assert test_class.ssl_verify is True + + +@patch.dict(os.environ, {"NAUTOBOT_VALIDATE_CERTS": "cheese"}) +def test_validate_certs_invalid(): + """Test that the default SSL verify is set as false via environment variable.""" + with pytest.raises(ValueError) as exc: + _ = NautobotApiBase(url="https://nautobot.example.com", token="abc123") + assert "Invalid truthy value" in str(exc.value) diff --git a/tests/unit/module_utils/test_utils.py b/tests/unit/module_utils/test_utils.py new file mode 100644 index 00000000..c83d1bcc --- /dev/null +++ b/tests/unit/module_utils/test_utils.py @@ -0,0 +1,54 @@ +"""Tests for module_utils functions.""" + +from typing import Any + +import pytest + +try: + from plugins.module_utils.utils import is_truthy +except ImportError: + import sys + + sys.path.append("plugins/module_utils") + sys.path.append("tests") + from utils import is_truthy + + +@pytest.mark.parametrize( + "value, expected", + [ + (True, True), + (False, False), + ("true", True), + ("false", False), + ("True", True), + ("False", False), + ("TRUE", True), + ("FALSE", False), + ("t", True), + ("f", False), + ("T", True), + ("F", False), + ("yes", True), + ("no", False), + ("Yes", True), + ("No", False), + ("YES", True), + ("NO", False), + ("y", True), + ("n", False), + ("Y", True), + ("N", False), + ("1", True), + ("0", False), + ], +) +def test_is_truthy(value: Any, expected: bool) -> None: + assert is_truthy(value) == expected + + +def test_is_truthy_raises_exception_on_invalid_type() -> None: + with pytest.raises(ValueError) as excinfo: + is_truthy("test") + + assert "Invalid truthy value" in str(excinfo.value)