Fix AWS Lightsail deployment error (boto3 parameter) (#14823)

* Fix AWS Lightsail deployment error by removing deprecated boto3 parameter

Remove the deprecated boto3 parameter from get_aws_connection_info() call
in the lightsail_region_facts module. This parameter has been non-functional
since amazon.aws collection 4.0.0 and was removed in recent versions bundled
with Ansible 11.x, causing deployment failures.

The function works correctly without this parameter as the module already
properly imports and validates boto3 availability.

Closes #14822

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update uv.lock to fix Docker build failure

The lockfile was out of sync after the Ansible 11.8.0 to 11.9.0 upgrade.
This regenerates the lockfile to include:
- ansible 11.9.0 (was 11.8.0)
- ansible-core 2.18.8 (was 2.18.7)

This fixes the Docker build CI failure where uv sync --locked was failing
due to lockfile mismatch.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Jinja spacing linter issues correctly

- Add spacing in lookup('env', 'VAR') calls
- Fix spacing around pipe operators within Jinja expressions only
- Preserve YAML block scalar syntax (prompt: |)
- Fix array indexing spacing within Jinja expressions
- All changes pass yamllint and ansible-lint tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add algo.egg-info to .gitignore

* Add unit test for AWS Lightsail boto3 parameter fix

- Tests that get_aws_connection_info() is called without boto3 parameter
- Verifies the module can be imported successfully
- Checks source code doesn't contain boto3=True
- Regression test specifically for issue #14822
- All 4 test cases pass

This ensures the fix remains in place and prevents regression.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix Python linting issues in test file

- Sort imports according to ruff standards
- Remove trailing whitespace from blank lines
- Remove unnecessary 'r' mode argument from open()
- Add trailing newline at end of file

All tests still pass after linting fixes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dan Guido 2025-08-16 03:39:00 -04:00 committed by GitHub
parent 55e4cab788
commit b821080eba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 280 additions and 122 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@ inventory_users
.ansible/ .ansible/
__pycache__/ __pycache__/
*.pyc *.pyc
algo.egg-info/

View file

@ -82,7 +82,7 @@ def main():
module.fail_json(msg='Python module "botocore" is missing, please install it') module.fail_json(msg='Python module "botocore" is missing, please install it')
try: try:
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
client = None client = None
try: try:

View file

@ -0,0 +1,157 @@
#!/usr/bin/env python
"""
Test for AWS Lightsail boto3 parameter fix.
Verifies that get_aws_connection_info() works without the deprecated boto3 parameter.
Addresses issue #14822.
"""
import importlib.util
import os
import sys
import unittest
from unittest.mock import MagicMock, patch
# Add the library directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../library'))
class TestLightsailBoto3Fix(unittest.TestCase):
"""Test that lightsail_region_facts.py works without boto3 parameter."""
def setUp(self):
"""Set up test fixtures."""
# Mock the ansible module_utils since we're testing outside of Ansible
self.mock_modules = {
'ansible.module_utils.basic': MagicMock(),
'ansible.module_utils.ec2': MagicMock(),
'ansible.module_utils.aws.core': MagicMock(),
}
# Apply mocks
self.patches = []
for module_name, mock_module in self.mock_modules.items():
patcher = patch.dict('sys.modules', {module_name: mock_module})
patcher.start()
self.patches.append(patcher)
def tearDown(self):
"""Clean up patches."""
for patcher in self.patches:
patcher.stop()
def test_lightsail_region_facts_imports(self):
"""Test that lightsail_region_facts can be imported."""
try:
# Import the module
spec = importlib.util.spec_from_file_location(
"lightsail_region_facts",
os.path.join(os.path.dirname(__file__), '../../library/lightsail_region_facts.py')
)
module = importlib.util.module_from_spec(spec)
# This should not raise an error
spec.loader.exec_module(module)
# Verify the module loaded
self.assertIsNotNone(module)
self.assertTrue(hasattr(module, 'main'))
except Exception as e:
self.fail(f"Failed to import lightsail_region_facts: {e}")
def test_get_aws_connection_info_called_without_boto3(self):
"""Test that get_aws_connection_info is called without boto3 parameter."""
# Mock get_aws_connection_info to track calls
mock_get_aws_connection_info = MagicMock(
return_value=('us-west-2', None, {})
)
with patch('ansible.module_utils.ec2.get_aws_connection_info', mock_get_aws_connection_info):
# Import the module
spec = importlib.util.spec_from_file_location(
"lightsail_region_facts",
os.path.join(os.path.dirname(__file__), '../../library/lightsail_region_facts.py')
)
module = importlib.util.module_from_spec(spec)
# Mock AnsibleModule
mock_ansible_module = MagicMock()
mock_ansible_module.params = {}
mock_ansible_module.check_mode = False
with patch('ansible.module_utils.basic.AnsibleModule', return_value=mock_ansible_module):
# Execute the module
try:
spec.loader.exec_module(module)
module.main()
except SystemExit:
# Module calls exit_json or fail_json which raises SystemExit
pass
except Exception:
# We expect some exceptions since we're mocking, but we want to check the call
pass
# Verify get_aws_connection_info was called
if mock_get_aws_connection_info.called:
# Get the call arguments
call_args = mock_get_aws_connection_info.call_args
# Ensure boto3=True is NOT in the arguments
if call_args:
# Check positional arguments
if call_args[0]: # args
self.assertTrue(len(call_args[0]) <= 1,
"get_aws_connection_info should be called with at most 1 positional arg (module)")
# Check keyword arguments
if call_args[1]: # kwargs
self.assertNotIn('boto3', call_args[1],
"get_aws_connection_info should not be called with boto3 parameter")
def test_no_boto3_parameter_in_source(self):
"""Verify that boto3 parameter is not present in the source code."""
lightsail_path = os.path.join(os.path.dirname(__file__), '../../library/lightsail_region_facts.py')
with open(lightsail_path) as f:
content = f.read()
# Check that boto3=True is not in the file
self.assertNotIn('boto3=True', content,
"boto3=True parameter should not be present in lightsail_region_facts.py")
# Check that boto3 parameter is not used with get_aws_connection_info
self.assertNotIn('get_aws_connection_info(module, boto3', content,
"get_aws_connection_info should not be called with boto3 parameter")
def test_regression_issue_14822(self):
"""
Regression test for issue #14822.
Ensures that the deprecated boto3 parameter is not used.
"""
# This test documents the specific issue that was fixed
# The boto3 parameter was deprecated and removed in amazon.aws collection
# that comes with Ansible 11.x
lightsail_path = os.path.join(os.path.dirname(__file__), '../../library/lightsail_region_facts.py')
with open(lightsail_path) as f:
lines = f.readlines()
# Find the line that calls get_aws_connection_info
for line_num, line in enumerate(lines, 1):
if 'get_aws_connection_info' in line and 'region' in line:
# This should be around line 85
# Verify it doesn't have boto3=True
self.assertNotIn('boto3', line,
f"Line {line_num} should not contain boto3 parameter")
# Verify the correct format
self.assertIn('get_aws_connection_info(module)', line,
f"Line {line_num} should call get_aws_connection_info(module) without boto3")
break
else:
self.fail("Could not find get_aws_connection_info call in lightsail_region_facts.py")
if __name__ == '__main__':
unittest.main()

14
uv.lock generated
View file

@ -68,7 +68,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "ansible", specifier = "==11.8.0" }, { name = "ansible", specifier = "==11.9.0" },
{ name = "azure-identity", marker = "extra == 'azure'", specifier = ">=1.15.0" }, { name = "azure-identity", marker = "extra == 'azure'", specifier = ">=1.15.0" },
{ name = "azure-mgmt-compute", marker = "extra == 'azure'", specifier = ">=30.0.0" }, { name = "azure-mgmt-compute", marker = "extra == 'azure'", specifier = ">=30.0.0" },
{ name = "azure-mgmt-network", marker = "extra == 'azure'", specifier = ">=25.0.0" }, { name = "azure-mgmt-network", marker = "extra == 'azure'", specifier = ">=25.0.0" },
@ -99,19 +99,19 @@ dev = [
[[package]] [[package]]
name = "ansible" name = "ansible"
version = "11.8.0" version = "11.9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "ansible-core" }, { name = "ansible-core" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/82/74/b86d14d2c458edf27ddb56d42bf6d07335a0ccfc713f040fb0cbffd30017/ansible-11.8.0.tar.gz", hash = "sha256:28ea032c77f344bb8ea4d7d39f9a5d4e935e6c8b60836c8c8a28b9cf6c9adb1a", size = 44286995, upload-time = "2025-07-16T15:13:22.91Z" } sdist = { url = "https://files.pythonhosted.org/packages/e8/b3/01564da36375f35907c2ec6626d68f4d59e39e566c4b3f874f01d0d2ca19/ansible-11.9.0.tar.gz", hash = "sha256:528ca5a408f11cf1fea00daea7570e68d40e167be38b90c119a7cb45729e4921", size = 44589149, upload-time = "2025-08-12T16:03:31.912Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/47/23fe9f6d9cd533ce4d54f4925cb0b7fdcfd3500b226421aad6166d9aa11c/ansible-11.8.0-py3-none-any.whl", hash = "sha256:a2cd44c0d2c03972f5d676d1b024d09dd3d3edbd418fb0426f4dd356fca9e5b1", size = 56046023, upload-time = "2025-07-16T15:13:17.557Z" }, { url = "https://files.pythonhosted.org/packages/84/fd/093dfe1f7f9f1058c0efa10b685f6049b676aa2c0ecd6f99c8664cafad6a/ansible-11.9.0-py3-none-any.whl", hash = "sha256:79b087ef38105b93e0e092e7013a0f840e154a6a8ce9b5fddd1b47593adc542a", size = 56340779, upload-time = "2025-08-12T16:03:26.18Z" },
] ]
[[package]] [[package]]
name = "ansible-core" name = "ansible-core"
version = "2.18.7" version = "2.18.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cryptography" }, { name = "cryptography" },
@ -120,9 +120,9 @@ dependencies = [
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "resolvelib" }, { name = "resolvelib" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/29/33/cd25e1af669941fbb5c3d7ac4494cf4a288cb11f53225648d552f8bd8e54/ansible_core-2.18.7.tar.gz", hash = "sha256:1a129bf9fcd5dca2b17e83ce77147ee2fbc3c51a4958970152897cc5b6d0aae7", size = 3090256, upload-time = "2025-07-15T17:49:24.074Z" } sdist = { url = "https://files.pythonhosted.org/packages/9b/78/8b8680eaf7b1990a8d4c26f25cdf2b2eabaae764a3d8e5299b1d61c7c385/ansible_core-2.18.8.tar.gz", hash = "sha256:b0766215a96a47ce39933d27e1e996ca2beb54cf1b3907c742d35c913b1f78cd", size = 3088636, upload-time = "2025-08-11T19:03:12.495Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/a7/568e51c20f49c9e76a555a876ed641ecc59df29e93868f24cdf8c3289f6a/ansible_core-2.18.7-py3-none-any.whl", hash = "sha256:ac42ecb480fb98890d338072f7298cd462fb2117da6700d989c7ae688962ba69", size = 2209456, upload-time = "2025-07-15T17:49:22.549Z" }, { url = "https://files.pythonhosted.org/packages/a8/59/aa2c1918224b054c9a0d93812c0b446fa8ddba155dc96c855189fae9c82b/ansible_core-2.18.8-py3-none-any.whl", hash = "sha256:b60bc23b2f11fd0559a79d10ac152b52f58a18ca1b7be0a620dfe87f34ced689", size = 2218673, upload-time = "2025-08-11T19:03:04.75Z" },
] ]
[[package]] [[package]]