From 6b6e17470c173709d8c3b519e649f3b6a9b39fe3 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 6 Aug 2021 15:49:47 +0200 Subject: [PATCH 1/3] Add require_boto3_at_least and require_botocore_at_least --- plugins/module_utils/core.py | 38 ++++ .../test_require_at_least.py | 211 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py diff --git a/plugins/module_utils/core.py b/plugins/module_utils/core.py index 35fc24df98b..962a6c908c4 100644 --- a/plugins/module_utils/core.py +++ b/plugins/module_utils/core.py @@ -244,6 +244,25 @@ def _gather_versions(self): return dict(boto3_version=boto3.__version__, botocore_version=botocore.__version__) + def require_boto3_at_least(self, desired, **kwargs): + """Check if the available boto3 version is greater than or equal to a desired version. + + calls fail_json() when the boto3 version is less than the desired + version + + Usage: + module.require_boto3_at_least("1.2.3", reason="to update tags") + module.require_boto3_at_least("1.1.1") + + :param desired the minimum desired version + :param reason why the version is required (optional) + """ + if not self.boto3_at_least(desired): + self._module.fail_json( + msg=missing_required_lib('boto3>={0}'.format(desired), **kwargs), + **self._gather_versions() + ) + def boto3_at_least(self, desired): """Check if the available boto3 version is greater than or equal to a desired version. @@ -255,6 +274,25 @@ def boto3_at_least(self, desired): existing = self._gather_versions() return LooseVersion(existing['boto3_version']) >= LooseVersion(desired) + def require_botocore_at_least(self, desired, **kwargs): + """Check if the available botocore version is greater than or equal to a desired version. + + calls fail_json() when the botocore version is less than the desired + version + + Usage: + module.require_botocore_at_least("1.2.3", reason="to update tags") + module.require_botocore_at_least("1.1.1") + + :param desired the minimum desired version + :param reason why the version is required (optional) + """ + if not self.botocore_at_least(desired): + self._module.fail_json( + msg=missing_required_lib('botocore>={0}'.format(desired), **kwargs), + **self._gather_versions() + ) + def botocore_at_least(self, desired): """Check if the available botocore version is greater than or equal to a desired version. diff --git a/tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py b/tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py new file mode 100644 index 00000000000..c0e0e5b6c00 --- /dev/null +++ b/tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py @@ -0,0 +1,211 @@ +# (c) 2021 Red Hat Inc. +# +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import botocore +import boto3 +import json + +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule + +DUMMY_VERSION = '5.5.5.5' + +TEST_VERSIONS = [ + ['1.1.1', '2.2.2', True], + ['1.1.1', '0.0.1', False], + ['9.9.9', '9.9.9', True], + ['9.9.9', '9.9.10', True], + ['9.9.9', '9.10.9', True], + ['9.9.9', '10.9.9', True], + ['9.9.9', '9.9.8', False], + ['9.9.9', '9.8.9', False], + ['9.9.9', '8.9.9', False], + ['10.10.10', '10.10.10', True], + ['10.10.10', '10.10.11', True], + ['10.10.10', '10.11.10', True], + ['10.10.10', '11.10.10', True], + ['10.10.10', '10.10.9', False], + ['10.10.10', '10.9.10', False], + ['10.10.10', '9.19.10', False], +] + + +class TestRequireAtLeast(object): + # ======================================================== + # Prepare some data for use in our testing + # ======================================================== + def setup_method(self): + pass + + # ======================================================== + # Test botocore_at_least + # ======================================================== + @pytest.mark.parametrize("stdin, desired_version, compare_version, at_least", [({}, *d) for d in TEST_VERSIONS], indirect=["stdin"]) + def test_botocore_at_least(self, monkeypatch, stdin, desired_version, compare_version, at_least, capfd): + monkeypatch.setattr(botocore, "__version__", compare_version) + # Set boto3 version to a known value (tests are on both sides) to make + # sure we're comparing the right library + monkeypatch.setattr(boto3, "__version__", DUMMY_VERSION) + + # Create a minimal module that we can call + module = AnsibleAWSModule(argument_spec=dict()) + + assert at_least == module.botocore_at_least(desired_version) + + # ======================================================== + # Test boto3_at_least + # ======================================================== + @pytest.mark.parametrize("stdin, desired_version, compare_version, at_least", [({}, *d) for d in TEST_VERSIONS], indirect=["stdin"]) + def test_boto3_at_least(self, monkeypatch, stdin, desired_version, compare_version, at_least, capfd): + # Set botocore version to a known value (tests are on both sides) to make + # sure we're comparing the right library + monkeypatch.setattr(botocore, "__version__", DUMMY_VERSION) + monkeypatch.setattr(boto3, "__version__", compare_version) + + # Create a minimal module that we can call + module = AnsibleAWSModule(argument_spec=dict()) + + assert at_least == module.boto3_at_least(desired_version) + + # ======================================================== + # Test require_botocore_at_least + # ======================================================== + @pytest.mark.parametrize("stdin, desired_version, compare_version, at_least", [({}, *d) for d in TEST_VERSIONS], indirect=["stdin"]) + def test_require_botocore_at_least(self, monkeypatch, stdin, desired_version, compare_version, at_least, capfd): + monkeypatch.setattr(botocore, "__version__", compare_version) + # Set boto3 version to a known value (tests are on both sides) to make + # sure we're comparing the right library + monkeypatch.setattr(boto3, "__version__", DUMMY_VERSION) + + # Create a minimal module that we can call + module = AnsibleAWSModule(argument_spec=dict()) + + with pytest.raises(SystemExit) as e: + module.require_botocore_at_least(desired_version) + module.exit_json() + + out, err = capfd.readouterr() + return_val = json.loads(out) + + assert return_val.get("exception") is None + assert return_val.get("invocation") is not None + if at_least: + assert return_val.get("failed") is None + else: + assert return_val.get("failed") + # The message is generated by Ansible, don't test for an exact + # message + assert desired_version in return_val.get("msg") + assert "botocore" in return_val.get("msg") + assert return_val.get("boto3_version") == DUMMY_VERSION + assert return_val.get("botocore_version") == compare_version + + # ======================================================== + # Test require_boto3_at_least + # ======================================================== + @pytest.mark.parametrize("stdin, desired_version, compare_version, at_least", [({}, *d) for d in TEST_VERSIONS], indirect=["stdin"]) + def test_require_boto3_at_least(self, monkeypatch, stdin, desired_version, compare_version, at_least, capfd): + monkeypatch.setattr(botocore, "__version__", DUMMY_VERSION) + # Set boto3 version to a known value (tests are on both sides) to make + # sure we're comparing the right library + monkeypatch.setattr(boto3, "__version__", compare_version) + + # Create a minimal module that we can call + module = AnsibleAWSModule(argument_spec=dict()) + + with pytest.raises(SystemExit) as e: + module.require_boto3_at_least(desired_version) + module.exit_json() + + out, err = capfd.readouterr() + return_val = json.loads(out) + + assert return_val.get("exception") is None + assert return_val.get("invocation") is not None + if at_least: + assert return_val.get("failed") is None + else: + assert return_val.get("failed") + # The message is generated by Ansible, don't test for an exact + # message + assert desired_version in return_val.get("msg") + assert "boto3" in return_val.get("msg") + assert return_val.get("botocore_version") == DUMMY_VERSION + assert return_val.get("boto3_version") == compare_version + + # ======================================================== + # Test require_botocore_at_least with reason + # ======================================================== + @pytest.mark.parametrize("stdin, desired_version, compare_version, at_least", [({}, *d) for d in TEST_VERSIONS], indirect=["stdin"]) + def test_require_botocore_at_least_with_reason(self, monkeypatch, stdin, desired_version, compare_version, at_least, capfd): + monkeypatch.setattr(botocore, "__version__", compare_version) + # Set boto3 version to a known value (tests are on both sides) to make + # sure we're comparing the right library + monkeypatch.setattr(boto3, "__version__", DUMMY_VERSION) + + reason = 'testing in progress' + + # Create a minimal module that we can call + module = AnsibleAWSModule(argument_spec=dict()) + + with pytest.raises(SystemExit) as e: + module.require_botocore_at_least(desired_version, reason=reason) + module.exit_json() + + out, err = capfd.readouterr() + return_val = json.loads(out) + + assert return_val.get("exception") is None + assert return_val.get("invocation") is not None + if at_least: + assert return_val.get("failed") is None + else: + assert return_val.get("failed") + # The message is generated by Ansible, don't test for an exact + # message + assert desired_version in return_val.get("msg") + assert " {0}".format(reason) in return_val.get("msg") + assert "botocore" in return_val.get("msg") + assert return_val.get("boto3_version") == DUMMY_VERSION + assert return_val.get("botocore_version") == compare_version + + # ======================================================== + # Test require_boto3_at_least with reason + # ======================================================== + @pytest.mark.parametrize("stdin, desired_version, compare_version, at_least", [({}, *d) for d in TEST_VERSIONS], indirect=["stdin"]) + def test_require_boto3_at_least_with_reason(self, monkeypatch, stdin, desired_version, compare_version, at_least, capfd): + monkeypatch.setattr(botocore, "__version__", DUMMY_VERSION) + # Set boto3 version to a known value (tests are on both sides) to make + # sure we're comparing the right library + monkeypatch.setattr(boto3, "__version__", compare_version) + + reason = 'testing in progress' + + # Create a minimal module that we can call + module = AnsibleAWSModule(argument_spec=dict()) + + with pytest.raises(SystemExit) as e: + module.require_boto3_at_least(desired_version, reason=reason) + module.exit_json() + + out, err = capfd.readouterr() + return_val = json.loads(out) + + assert return_val.get("exception") is None + assert return_val.get("invocation") is not None + if at_least: + assert return_val.get("failed") is None + else: + assert return_val.get("failed") + # The message is generated by Ansible, don't test for an exact + # message + assert desired_version in return_val.get("msg") + assert " {0}".format(reason) in return_val.get("msg") + assert "boto3" in return_val.get("msg") + assert return_val.get("botocore_version") == DUMMY_VERSION + assert return_val.get("boto3_version") == compare_version From e5099bf7476108d2ac3dad806ef9222dc542ec04 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 6 Aug 2021 17:03:24 +0200 Subject: [PATCH 2/3] Don't run the 2.6/2.7 compile sanity tests against the new integration test --- tests/sanity/ignore-2.10.txt | 4 +++- tests/sanity/ignore-2.11.txt | 4 +++- tests/sanity/ignore-2.12.txt | 2 +- tests/sanity/ignore-2.9.txt | 2 ++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 77d93927cf4..21386a99d29 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -374,4 +374,6 @@ plugins/modules/s3_bucket.py import-3.5!skip plugins/modules/s3_bucket.py import-3.6!skip plugins/modules/s3_bucket.py import-3.7!skip plugins/modules/s3_bucket.py metaclass-boilerplate!skip -tests/sanity/refresh_ignore_files shebang!skip \ No newline at end of file +tests/sanity/refresh_ignore_files shebang!skip +tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py compile-2.6!skip +tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py compile-2.7!skip diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 77d93927cf4..21386a99d29 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -374,4 +374,6 @@ plugins/modules/s3_bucket.py import-3.5!skip plugins/modules/s3_bucket.py import-3.6!skip plugins/modules/s3_bucket.py import-3.7!skip plugins/modules/s3_bucket.py metaclass-boilerplate!skip -tests/sanity/refresh_ignore_files shebang!skip \ No newline at end of file +tests/sanity/refresh_ignore_files shebang!skip +tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py compile-2.6!skip +tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py compile-2.7!skip diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 77d93927cf4..58f2487df16 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -374,4 +374,4 @@ plugins/modules/s3_bucket.py import-3.5!skip plugins/modules/s3_bucket.py import-3.6!skip plugins/modules/s3_bucket.py import-3.7!skip plugins/modules/s3_bucket.py metaclass-boilerplate!skip -tests/sanity/refresh_ignore_files shebang!skip \ No newline at end of file +tests/sanity/refresh_ignore_files shebang!skip diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index c67a1d687a2..b7a6434ca16 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -392,3 +392,5 @@ plugins/modules/s3_bucket.py import-3.6!skip plugins/modules/s3_bucket.py import-3.7!skip plugins/modules/s3_bucket.py metaclass-boilerplate!skip tests/sanity/refresh_ignore_files shebang!skip +tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py compile-2.6!skip +tests/unit/module_utils/core/ansible_aws_module/test_require_at_least.py compile-2.7!skip From 9ea024969f73bf7222ce4c4530d977174181072a Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 6 Aug 2021 16:10:22 +0200 Subject: [PATCH 3/3] Add example of using require_botocore_at_least --- plugins/modules/ec2_vol.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/modules/ec2_vol.py b/plugins/modules/ec2_vol.py index d459e4c547b..238f214e47f 100644 --- a/plugins/modules/ec2_vol.py +++ b/plugins/modules/ec2_vol.py @@ -719,8 +719,7 @@ def main(): 'Using the "list" state has been deprecated. Please use the ec2_vol_info module instead', date='2022-06-01', collection_name='amazon.aws') if module.params.get('throughput'): - if not module.botocore_at_least("1.19.27"): - module.fail_json(msg="botocore >= 1.19.27 is required to set the throughput for a volume") + module.require_botocore_at_least('1.19.27', reason='to set the throughput for a volume') # Ensure we have the zone or can get the zone if instance is None and zone is None and state == 'present':