Repository: ansible-collections/community.aws
Pull request #2275: refactor connection/aws_ssm plugin
Author: login: abikouo
Labels: name: mergeit
description: Merge the PR (SoftwareFactory)
name: backport-9
description: PR should be backported to the stable-9 branch
Created: 2025-04-01T16:13:33Z | Merged: 2025-04-11T06:55:39Z

##### SUMMARY
Refactor S3 operation function from `connection/aws_ssm` plugin
Refer to: https://issues.redhat.com/browse/ACA-2100

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
- Feature Pull Request



Diff:
diff --git a/changelogs/fragments/20250104-refactor-connection-aws_ssm-s3-operation.yml b/changelogs/fragments/20250104-refactor-connection-aws_ssm-s3-operation.yml
new file mode 100644
index 00000000000..d4ee562cae4
--- /dev/null
+++ b/changelogs/fragments/20250104-refactor-connection-aws_ssm-s3-operation.yml
@@ -0,0 +1,2 @@
+minor_changes:
+  - aws_ssm - Refactor S3 operations methods for improved clarity (https://github.com/ansible-collections/community.aws/pull/2275).
diff --git a/plugins/connection/aws_ssm.py b/plugins/connection/aws_ssm.py
index 17ecaa4983b..2fa46adfc3e 100644
--- a/plugins/connection/aws_ssm.py
+++ b/plugins/connection/aws_ssm.py
@@ -334,20 +334,12 @@
 import time
 from functools import wraps
 from typing import Any
-from typing import Dict
 from typing import Iterator
 from typing import List
 from typing import NoReturn
-from typing import Optional
 from typing import Tuple
 from typing import TypedDict
 
-try:
-    import boto3
-    from botocore.client import Config
-except ImportError:
-    pass
-
 from ansible.errors import AnsibleConnectionFailure
 from ansible.errors import AnsibleError
 from ansible.errors import AnsibleFileNotFound
@@ -359,7 +351,7 @@
 from ansible.utils.display import Display
 
 from ansible_collections.amazon.aws.plugins.module_utils.botocore import HAS_BOTO3
-
+from ansible_collections.community.aws.plugins.plugin_utils.base import AwsConnectionPluginBase
 from ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager import S3ClientManager
 from ansible_collections.community.aws.plugins.plugin_utils.terminalmanager import TerminalManager
 
@@ -455,7 +447,7 @@ class CommandResult(TypedDict):
     stderr_combined: str
 
 
-class Connection(ConnectionBase):
+class Connection(ConnectionBase, AwsConnectionPluginBase):
     """AWS SSM based connections"""
 
     transport = "community.aws.aws_ssm"
@@ -467,7 +459,6 @@ class Connection(ConnectionBase):
     is_windows = False
 
     _client = None
-    _s3_client = None
     _session = None
     _stdout = None
     _session_id = ""
@@ -514,40 +505,36 @@ def _init_clients(self) -> None:
         """
 
         self.verbosity_display(4, "INITIALIZE BOTO3 CLIENTS")
-        profile_name = self.get_option("profile") or ""
-        region_name = self.get_option("region")
-
-        # Initialize S3ClientManager
-        self.s3_manager = S3ClientManager(self)
 
-        # Initialize S3 client
-        s3_endpoint_url, s3_region_name = self.s3_manager.get_bucket_endpoint()
-        self.verbosity_display(4, f"SETUP BOTO3 CLIENTS: S3 {s3_endpoint_url}")
-        self.s3_manager.initialize_client(
-            region_name=s3_region_name, endpoint_url=s3_endpoint_url, profile_name=profile_name
+        # Create S3 and SSM clients
+        config = {"signature_version": "s3v4", "s3": {"addressing_style": self.get_option("s3_addressing_style")}}
+
+        bucket_endpoint_url = self.get_option("bucket_endpoint_url")
+        s3_endpoint_url, s3_region_name = S3ClientManager.get_bucket_endpoint(
+            bucket_name=self.get_option("bucket_name"),
+            bucket_endpoint_url=bucket_endpoint_url,
+            access_key_id=self.get_option("access_key_id"),
+            secret_key_id=self.get_option("secret_access_key"),
+            session_token=self.get_option("session_token"),
+            region_name=self.get_option("region"),
+            profile_name=self.get_option("profile"),
         )
-        self._s3_client = self.s3_manager._s3_client
 
-        # Initialize SSM client
-        self._initialize_ssm_client(region_name, profile_name)
+        self.verbosity_display(4, f"BUCKET Information - Endpoint: {s3_endpoint_url} / Region: {s3_region_name}")
 
-    def _initialize_ssm_client(self, region_name: Optional[str], profile_name: str) -> None:
-        """
-        Initializes the SSM client used to manage sessions.
-        Args:
-            region_name (Optional[str]): AWS region for the SSM client.
-            profile_name (str): AWS profile name for authentication.
+        # Initialize S3ClientManager
+        s3_client = self._get_boto_client("s3", endpoint_url=s3_endpoint_url, region_name=s3_region_name, config=config)
+        self.s3_manager = S3ClientManager(s3_client)
 
-        Returns:
-            None
-        """
+        # Initialize SSM client
+        self._client = self._get_boto_client("ssm", region_name=self.get_option("region"), config=config)
 
-        self.verbosity_display(4, "SETUP BOTO3 CLIENTS: SSM")
-        self._client = self._get_boto_client(
-            "ssm",
-            region_name=region_name,
-            profile_name=profile_name,
-        )
+    @property
+    def s3_client(self) -> None:
+        client = None
+        if self.s3_manager is not None:
+            client = self.s3_manager.client
+        return client
 
     def verbosity_display(self, level: int, message: str) -> None:
         """
@@ -616,6 +603,7 @@ def start_session(self):
         if document_name is not None:
             start_session_args["DocumentName"] = document_name
         response = self._client.start_session(**start_session_args)
+        self.verbosity_display(4, f"START SESSION RESPONSE: {response}")
         self._session_id = response["SessionId"]
 
         region_name = self.get_option("region")
@@ -803,153 +791,26 @@ def _flush_stderr(self, session_process) -> str:
 
         return stderr
 
-    def _get_boto_client(self, service, region_name=None, profile_name=None, endpoint_url=None):
-        """Gets a boto3 client based on the STS token"""
-
-        aws_access_key_id = self.get_option("access_key_id")
-        aws_secret_access_key = self.get_option("secret_access_key")
-        aws_session_token = self.get_option("session_token")
-
-        session_args = dict(
-            aws_access_key_id=aws_access_key_id,
-            aws_secret_access_key=aws_secret_access_key,
-            aws_session_token=aws_session_token,
-            region_name=region_name,
-        )
-        if profile_name:
-            session_args["profile_name"] = profile_name
-        session = boto3.session.Session(**session_args)
-
-        client = session.client(
-            service,
-            endpoint_url=endpoint_url,
-            config=Config(
-                signature_version="s3v4",
-                s3={"addressing_style": self.get_option("s3_addressing_style")},
-            ),
-        )
-        return client
-
     def _escape_path(self, path: str) -> str:
         return path.replace("\\", "/")
 
-    def _generate_commands(
-        self,
-        bucket_name: str,
-        s3_path: str,
-        in_path: str,
-        out_path: str,
-    ) -> Tuple[List[Dict], dict]:
-        """
-        Generate commands for the specified bucket, S3 path, input path, and output path.
-
-        :param bucket_name: The name of the S3 bucket used for file transfers.
-        :param s3_path: The S3 path to the file to be sent.
-        :param in_path: Input path
-        :param out_path: Output path
-        :param method: The request method to use for the command (can be "get" or "put").
-
-        :returns: A tuple containing a list of command dictionaries along with any ``put_args`` dictionaries.
-        """
-
-        put_args, put_headers = self.s3_manager.generate_encryption_settings()
-        commands = []
-
-        put_url = self.s3_manager.get_url("put_object", bucket_name, s3_path, "PUT", extra_args=put_args)
-        get_url = self.s3_manager.get_url("get_object", bucket_name, s3_path, "GET")
-
-        if self.is_windows:
-            put_command_headers = "; ".join([f"'{h}' = '{v}'" for h, v in put_headers.items()])
-            commands.append({
-                "command":
-                (
-                    "Invoke-WebRequest "
-                    f"'{get_url}' "
-                    f"-OutFile '{out_path}'"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are get_commands
-                "method": "get",
-                "headers": {},
-            })  # fmt: skip
-            commands.append({
-                "command":
-                (
-                    "Invoke-WebRequest -Method PUT "
-                    # @{'key' = 'value'; 'key2' = 'value2'}
-                    f"-Headers @{{{put_command_headers}}} "
-                    f"-InFile '{in_path}' "
-                    f"-Uri '{put_url}' "
-                    f"-UseBasicParsing"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are put_commands
-                "method": "put",
-                "headers": put_headers,
-            })  # fmt: skip
-        else:
-            put_command_headers = " ".join([f"-H '{h}: {v}'" for h, v in put_headers.items()])
-            commands.append({
-                "command":
-                (
-                    "curl "
-                    f"-o '{out_path}' "
-                    f"'{get_url}'"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are get_commands
-                "method": "get",
-                "headers": {},
-            })  # fmt: skip
-            # Due to https://github.com/curl/curl/issues/183 earlier
-            # versions of curl did not create the output file, when the
-            # response was empty. Although this issue was fixed in 2015,
-            # some actively maintained operating systems still use older
-            # versions of it (e.g. CentOS 7)
-            commands.append({
-                "command":
-                (
-                    "touch "
-                    f"'{out_path}'"
-                ),
-                "method": "get",
-                "headers": {},
-            })  # fmt: skip
-            commands.append({
-                "command":
-                (
-                    "curl --request PUT "
-                    f"{put_command_headers} "
-                    f"--upload-file '{in_path}' "
-                    f"'{put_url}'"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are put_commands
-                "method": "put",
-                "headers": put_headers,
-            })  # fmt: skip
-
-        return commands, put_args
-
-    def _exec_transport_commands(self, in_path: str, out_path: str, commands: List[dict]) -> CommandResult:
+    def _exec_transport_commands(self, in_path: str, out_path: str, command: dict) -> CommandResult:
         """
-        Execute the provided transport commands.
+        Execute the provided transport command.
 
         :param in_path: The input path.
         :param out_path: The output path.
-        :param commands: A list of command dictionaries containing the command string and metadata.
+        :param command: A command to execute on the host.
 
         :returns: A tuple containing the return code, stdout, and stderr.
         """
 
-        stdout_combined, stderr_combined = "", ""
-        for command in commands:
-            (returncode, stdout, stderr) = self.exec_command(command["command"], in_data=None, sudoable=False)
-
-            # Check the return code
-            if returncode != 0:
-                raise AnsibleError(f"failed to transfer file to {in_path} {out_path}:\n{stdout}\n{stderr}")
+        returncode, stdout, stderr = self.exec_command(command, in_data=None, sudoable=False)
+        # Check the return code
+        if returncode != 0:
+            raise AnsibleError(f"failed to transfer file to {in_path} {out_path}:\n{stdout}\n{stderr}")
 
-            stdout_combined += stdout
-            stderr_combined += stderr
-
-        return (returncode, stdout_combined, stderr_combined)
+        return returncode, stdout, stderr
 
     @_ssm_retry
     def _file_transport_command(
@@ -971,30 +832,30 @@ def _file_transport_command(
         bucket_name = self.get_option("bucket_name")
         s3_path = self._escape_path(f"{self.instance_id}/{out_path}")
 
-        client = self._s3_client
-
-        commands, put_args = self._generate_commands(
+        command, put_args = self.s3_manager.generate_host_commands(
             bucket_name,
+            self.get_option("bucket_sse_mode"),
+            self.get_option("bucket_sse_kms_key_id"),
             s3_path,
             in_path,
             out_path,
+            self.is_windows,
+            ssm_action,
         )
 
         try:
             if ssm_action == "get":
-                put_commands = [cmd for cmd in commands if cmd.get("method") == "put"]
-                result = self._exec_transport_commands(in_path, out_path, put_commands)
+                result = self._exec_transport_commands(in_path, out_path, command)
                 with open(to_bytes(out_path, errors="surrogate_or_strict"), "wb") as data:
-                    client.download_fileobj(bucket_name, s3_path, data)
+                    self.s3_client.download_fileobj(bucket_name, s3_path, data)
             else:
-                get_commands = [cmd for cmd in commands if cmd.get("method") == "get"]
                 with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as data:
-                    client.upload_fileobj(data, bucket_name, s3_path, ExtraArgs=put_args)
-                result = self._exec_transport_commands(in_path, out_path, get_commands)
+                    self.s3_client.upload_fileobj(data, bucket_name, s3_path, ExtraArgs=put_args)
+                result = self._exec_transport_commands(in_path, out_path, command)
             return result
         finally:
             # Remove the files from the bucket after they've been transferred
-            client.delete_object(Bucket=bucket_name, Key=s3_path)
+            self.s3_client.delete_object(Bucket=bucket_name, Key=s3_path)
 
     def put_file(self, in_path: str, out_path: str) -> Tuple[int, str, str]:
         """transfer a file from local to remote"""
@@ -1019,12 +880,14 @@ def close(self) -> None:
         """terminate the connection"""
         if self._session_id:
             self.verbosity_display(3, f"CLOSING SSM CONNECTION TO: {self.instance_id}")
-            if self._has_timeout:
-                self._session.terminate()
-            else:
-                cmd = b"\nexit\n"
-                self._session.communicate(cmd)
+            if self._session is not None:
+                if self._has_timeout:
+                    self._session.terminate()
+                else:
+                    cmd = b"\nexit\n"
+                    self._session.communicate(cmd)
 
             self.verbosity_display(4, f"TERMINATE SSM SESSION: {self._session_id}")
-            self._client.terminate_session(SessionId=self._session_id)
+            if self._client:
+                self._client.terminate_session(SessionId=self._session_id)
             self._session_id = ""
diff --git a/plugins/plugin_utils/base.py b/plugins/plugin_utils/base.py
new file mode 100644
index 00000000000..7ce1eb7860a
--- /dev/null
+++ b/plugins/plugin_utils/base.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from typing import Any
+from typing import Dict
+from typing import Optional
+
+try:
+    from boto3.session import Session
+    from botocore.client import Config
+except ImportError:
+    pass
+
+
+class AwsConnectionPluginBase:
+    def __init__(self) -> None:
+        pass
+
+    def _get_boto_client(
+        self,
+        service: str,
+        region_name: Optional[str] = None,
+        endpoint_url: Optional[str] = None,
+        config: Optional[Dict[str, Any]] = None,
+    ) -> Any:
+        """Gets a boto3 client based on the STS token"""
+
+        aws_access_key_id = self.get_option("access_key_id")
+        aws_secret_access_key = self.get_option("secret_access_key")
+        aws_session_token = self.get_option("session_token")
+
+        session_args = dict(
+            aws_access_key_id=aws_access_key_id,
+            aws_secret_access_key=aws_secret_access_key,
+            aws_session_token=aws_session_token,
+            region_name=region_name,
+        )
+        profile_name = self.get_option("profile")
+        if profile_name:
+            session_args["profile_name"] = profile_name
+        session = Session(**session_args)
+        params = {}
+        if endpoint_url:
+            params["endpoint_url"] = endpoint_url
+        if config:
+            params["config"] = Config(**config)
+
+        return session.client(service, **params)
diff --git a/plugins/plugin_utils/s3clientmanager.py b/plugins/plugin_utils/s3clientmanager.py
index bfff38fedca..56561434c4d 100644
--- a/plugins/plugin_utils/s3clientmanager.py
+++ b/plugins/plugin_utils/s3clientmanager.py
@@ -5,7 +5,6 @@
 
 try:
     import boto3
-    from botocore.client import Config
 except ImportError:
     pass
 from typing import Any
@@ -14,84 +13,91 @@
 from typing import Tuple
 
 
-class S3ClientManager:
-    def __init__(self, connection) -> None:
-        self.connection = connection
-        self._s3_client = None
+def generate_encryption_settings(
+    bucket_sse_mode: Optional[str], bucket_sse_kms_key_id: Optional[str]
+) -> Tuple[Dict, Dict]:
+    """Generate Encryption Settings"""
+    args = {}
+    headers = {}
+    if not bucket_sse_mode:
+        return args, headers
 
-    def initialize_client(self, region_name: str, endpoint_url: str, profile_name: str) -> None:
-        """
-        Create the S3 client inside the manager.
-        """
-        self._s3_client = self.get_s3_client(
+    args["ServerSideEncryption"] = bucket_sse_mode
+    headers["x-amz-server-side-encryption"] = bucket_sse_mode
+    if bucket_sse_mode == "aws:kms" and bucket_sse_kms_key_id:
+        args["SSEKMSKeyId"] = bucket_sse_kms_key_id
+        headers["x-amz-server-side-encryption-aws-kms-key-id"] = bucket_sse_kms_key_id
+    return args, headers
+
+
+class S3ClientManager:
+    def __init__(self, client: Any) -> None:
+        self._s3_client = client
+
+    @property
+    def client(self) -> Any:
+        return self._s3_client
+
+    @staticmethod
+    def _get_s3_client(
+        access_key_id: Optional[str],
+        secret_key_id: Optional[str],
+        session_token: Optional[str],
+        region_name: str,
+        profile_name: Optional[str],
+    ) -> Any:
+        if not region_name:
+            region_name = "us-east-1"
+        session_args = dict(
+            aws_access_key_id=access_key_id,
+            aws_secret_access_key=secret_key_id,
+            aws_session_token=session_token,
             region_name=region_name,
-            endpoint_url=endpoint_url,
-            profile_name=profile_name,
         )
+        if profile_name:
+            session_args["profile_name"] = profile_name
+        session = boto3.session.Session(**session_args)
+        return session.client("s3")
 
-    def get_bucket_endpoint(self) -> Tuple[str, str]:
+    @staticmethod
+    def get_bucket_endpoint(
+        bucket_name: str,
+        bucket_endpoint_url: Optional[str],
+        access_key_id: Optional[str],
+        secret_key_id: Optional[str],
+        session_token: Optional[str],
+        region_name: Optional[str],
+        profile_name: Optional[str],
+    ) -> Tuple[str, str]:
         """
         Fetches the correct S3 endpoint and region for use with our bucket.
         If we don't explicitly set the endpoint then some commands will use the global
         endpoint and fail
         (new AWS regions and new buckets in a region other than the one we're running in)
         """
-        region_name = self.connection.get_option("region") or "us-east-1"
-        profile_name = self.connection.get_option("profile") or ""
-        self.connection.verbosity_display(4, "_get_bucket_endpoint: S3 (global)")
-        tmp_s3_client = self.get_s3_client(
-            region_name=region_name,
-            profile_name=profile_name,
+        if not region_name:
+            region_name = "us-east-1"
+        tmp_s3_client = S3ClientManager._get_s3_client(
+            access_key_id, secret_key_id, session_token, region_name, profile_name
         )
+
         # Fetch the location of the bucket so we can open a client against the 'right' endpoint
         # This /should/ always work
-        head_bucket = tmp_s3_client.head_bucket(
-            Bucket=(self.connection.get_option("bucket_name")),
-        )
+        head_bucket = tmp_s3_client.head_bucket(Bucket=(bucket_name))
         bucket_region = head_bucket.get("ResponseMetadata", {}).get("HTTPHeaders", {}).get("x-amz-bucket-region", None)
         if bucket_region is None:
             bucket_region = "us-east-1"
 
-        if self.connection.get_option("bucket_endpoint_url"):
-            return self.connection.get_option("bucket_endpoint_url"), bucket_region
+        if bucket_endpoint_url:
+            return bucket_endpoint_url, bucket_region
 
         # Create another client for the region the bucket lives in, so we can nab the endpoint URL
-        self.connection.verbosity_display(4, f"_get_bucket_endpoint: S3 (bucket region) - {bucket_region}")
-        s3_bucket_client = self.get_s3_client(
-            region_name=bucket_region,
-            profile_name=profile_name,
-        )
-
-        return s3_bucket_client.meta.endpoint_url, s3_bucket_client.meta.region_name
-
-    def get_s3_client(
-        self,
-        region_name: Optional[str] = None,
-        profile_name: Optional[str] = None,
-        endpoint_url: Optional[str] = None,
-    ) -> Any:
-        """Creates and returns a boto3 "s3" client"""
-
-        session_args = dict(
-            aws_access_key_id=self.connection.get_option("access_key_id"),
-            aws_secret_access_key=self.connection.get_option("secret_access_key"),
-            aws_session_token=self.connection.get_option("session_token"),
-            region_name=region_name,
-        )
-        if profile_name:
-            session_args["profile_name"] = profile_name
-
-        session = boto3.session.Session(**session_args)
+        if bucket_region != region_name:
+            tmp_s3_client = S3ClientManager._get_s3_client(
+                access_key_id, secret_key_id, session_token, bucket_region, profile_name
+            )
 
-        s3_client = session.client(
-            "s3",
-            endpoint_url=endpoint_url,
-            config=Config(
-                signature_version="s3v4",
-                s3={"addressing_style": self.connection.get_option("s3_addressing_style")},
-            ),
-        )
-        return s3_client
+        return tmp_s3_client.meta.endpoint_url, tmp_s3_client.meta.region_name
 
     def get_url(
         self,
@@ -110,20 +116,79 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[str, Optional[Dict[str, str]]]:
+        """
+        Generate commands for the specified bucket and configurations, S3 path, input path, and output path.
+        The function generates a curl/Invoke-WebRequest command to be executed on the managed node.
+        The method 'get' means the controller node wants to retrieve content from the managed node, the corresponding
+        on the managed node is a 'put' command to updload content to the S3 bucket.
+        The method 'put' means the controller node wants to upload content to the managed node, the corresponding
+        on the managed node is a 'get' command to download content to the S3 bucket.
+
+        :param bucket_name: The name of the S3 bucket used for file transfers.
+        :param s3_path: The S3 path to the file to be sent.
+        :param in_path: Input path
+        :param out_path: Output path
+        :param method: The request method to use for the command (can be "get" or "put").
+
+        :returns: A tuple containing a shell command string and optional extra arguments for the PUT request.
+        """
+
+        command = None
+        url = None
+        put_args = None
+        if method == "get":
+            put_args, put_headers = generate_encryption_settings(bucket_sse_mode, bucket_sse_kms_key_id)
+            url = self.get_url("put_object", bucket_name, s3_path, "PUT", extra_args=put_args)
+            if is_windows:
+                put_command_headers = "; ".join([f"'{h}' = '{v}'" for h, v in put_headers.items()])
+                command = (
+                    "Invoke-WebRequest -Method PUT "
+                    # @{'key' = 'value'; 'key2' = 'value2'}
+                    f"-Headers @{{{put_command_headers}}} "
+                    f"-InFile '{in_path}' "
+                    f"-Uri '{url}' "
+                    f"-UseBasicParsing"
+                )  # fmt: skip
+            else:
+                # Due to https://github.com/curl/curl/issues/183 earlier
+                # versions of curl did not create the output file, when the
+                # response was empty. Although this issue was fixed in 2015,
+                # some actively maintained operating systems still use older
+                # versions of it (e.g. CentOS 7)
+                put_command_headers = " ".join([f"-H '{h}: {v}'" for h, v in put_headers.items()])
+                command = (
+                    "curl --request PUT "
+                    f"{put_command_headers} "
+                    f"--upload-file '{in_path}' "
+                    f"'{url}'"
+                )  # fmt: skip
+        elif method == "put":
+            url = self.get_url("get_object", bucket_name, s3_path, "GET")
+            if is_windows:
+                command = (
+                    "Invoke-WebRequest "
+                    f"'{url}' "
+                    f"-OutFile '{out_path}'"
+                )  # fmt: skip
+            else:
+                command = (
+                    "curl "
+                    f"-o '{out_path}' "
+                    f"'{url}'"
+                    ";"
+                    "touch "
+                    f"'{out_path}'"
+                )  # fmt: skip
+
+        return command, put_args
diff --git a/tests/unit/plugins/connection/aws_ssm/test_aws_ssm.py b/tests/unit/plugins/connection/aws_ssm/test_aws_ssm.py
index 191dc6d56cb..37993a80e42 100644
--- a/tests/unit/plugins/connection/aws_ssm/test_aws_ssm.py
+++ b/tests/unit/plugins/connection/aws_ssm/test_aws_ssm.py
@@ -1,359 +1,266 @@
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
 from io import StringIO
+from unittest.mock import ANY
 from unittest.mock import MagicMock
+from unittest.mock import call
 from unittest.mock import patch
 
 import pytest
 
 from ansible.errors import AnsibleError
+from ansible.errors import AnsibleFileNotFound
 from ansible.playbook.play_context import PlayContext
 from ansible.plugins.loader import connection_loader
 
 from ansible_collections.amazon.aws.plugins.module_utils.botocore import HAS_BOTO3
 
-from ansible_collections.community.aws.plugins.connection.aws_ssm import Connection
 from ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager import S3ClientManager
+from ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager import generate_encryption_settings
 
 if not HAS_BOTO3:
     pytestmark = pytest.mark.skip("test_data_pipeline.py requires the python modules 'boto3' and 'botocore'")
 
 
+@pytest.fixture(name="loaded_aws_ssm")
+def fixture_loaded_aws_ssm():
+    conn = connection_loader.get("community.aws.aws_ssm", PlayContext(), StringIO())
+    # conn.verbosity_display = MagicMock()
+    conn._connect = MagicMock()
+    conn.test_options = {
+        "bucket_sse_mode": MagicMock(),
+        "bucket_sse_kms_key_id": MagicMock(),
+        "reconnection_retries": 3,
+    }
+    conn.get_option = MagicMock(side_effect=conn.test_options.get)
+    return conn
+
+
 class TestConnectionBaseClass:
     @patch(
         "ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.S3ClientManager.get_bucket_endpoint",
         return_value=("fake-s3-endpoint", "fake-region"),
     )
-    @patch(
-        "ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.S3ClientManager.get_s3_client",
-        return_value=MagicMock(),
-    )
-    def test_init_clients(self, mock_get_s3_client, mock_get_bucket_endpoint):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        def mock_get_option(key):
-            options = {
-                "profile": "test-profile",
-                "region": "us-east-1",
-            }
-            return options.get(key, None)
-
-        conn.get_option = MagicMock(side_effect=mock_get_option)
-
-        # Mock the _initialize_ssm_client and _initialize_s3_client methods
-        conn._initialize_ssm_client = MagicMock()
-
-        conn._init_clients()
-
-        # Assert that _initialize_ssm_client was called once
-        conn._initialize_ssm_client.assert_called_once_with("us-east-1", "test-profile")
+    def test_init_clients(self, mock_get_bucket_endpoint, loaded_aws_ssm):
+        boto_clients = {"ssm": MagicMock(), "s3": MagicMock()}
 
-        # Assert that get_bucket_endpoint was called once
-        mock_get_bucket_endpoint.assert_called_once()
+        print(dir(self))
 
-        # Assert that get_s3_client was called with appropriate arguments
-        mock_get_s3_client.assert_called_once()
+        def mock_get_boto_client(service, *args, **kwargs):
+            return boto_clients.get(service)
 
-        # Assert that self._s3_client is not None
-        assert conn._s3_client is not None
-        assert conn._s3_client is conn.s3_manager._s3_client
+        loaded_aws_ssm._get_boto_client = MagicMock(side_effect=mock_get_boto_client)
 
-    @patch("boto3.client")
-    def test_initialize_ssm_client(self, mock_boto3_client):
-        """
-        Test for the _initialize_ssm_client method to ensure the SSM client is initialized correctly.
-        """
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
+        options = {
+            "s3_addressing_style": MagicMock(),
+            "bucket_endpoint_url": MagicMock(),
+            "bucket_name": MagicMock(),
+            "access_key_id": MagicMock(),
+            "secret_access_key": MagicMock(),
+            "session_token": MagicMock(),
+            "region": MagicMock(),
+            "profile": MagicMock(),
+        }
 
-        test_region_name = "us-west-2"
-        test_profile_name = "test-profile"
+        def mock_get_option(name):
+            return options.get(name)
 
-        # Mock the _get_boto_client method to return a mock client
-        conn._get_boto_client = MagicMock(return_value=mock_boto3_client)
+        loaded_aws_ssm.get_option = MagicMock(side_effect=mock_get_option)
+        s3_endpoint_url, s3_region_name = MagicMock(), MagicMock()
+        mock_get_bucket_endpoint.return_value = (s3_endpoint_url, s3_region_name)
 
-        conn._initialize_ssm_client(test_region_name, test_profile_name)
+        loaded_aws_ssm._init_clients()
 
-        conn._get_boto_client.assert_called_once_with(
-            "ssm",
-            region_name=test_region_name,
-            profile_name=test_profile_name,
+        # Validate results
+        mock_get_bucket_endpoint.assert_called_once_with(
+            bucket_name=options.get("bucket_name"),
+            bucket_endpoint_url=options.get("bucket_endpoint_url"),
+            access_key_id=options.get("access_key_id"),
+            secret_key_id=options.get("secret_access_key"),
+            session_token=options.get("session_token"),
+            region_name=options.get("region"),
+            profile_name=options.get("profile"),
         )
 
-        assert conn._client is mock_boto3_client
-
-    @patch("os.path.exists")
-    @patch("subprocess.Popen")
-    @patch("select.poll")
-    @patch("boto3.client")
-    def test_plugins_connection_aws_ssm_start_session(self, boto_client, s_poll, s_popen, mock_ospe):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.get_option = MagicMock()
-        conn.get_option.side_effect = ["i1234", "executable", "abcd", "i1234"]
-        conn.host = "abc"
-        mock_ospe.return_value = True
-        boto3 = MagicMock()
-        boto3.client("ssm").return_value = MagicMock()
-        conn.start_session = MagicMock()
-        conn._session_id = MagicMock()
-        conn._session_id.return_value = "s1"
-        s_popen.return_value.stdin.write = MagicMock()
-        s_poll.return_value = MagicMock()
-        s_poll.return_value.register = MagicMock()
-        s_popen.return_value.poll = MagicMock()
-        s_popen.return_value.poll.return_value = None
-        conn._stdin_readline = MagicMock()
-        conn._stdin_readline.return_value = "abc123"
-        conn.SESSION_START = "abc"
-        conn.start_session()
-
-    @patch("random.choice")
-    def test_plugins_connection_aws_ssm_exec_command(self, r_choice):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        r_choice.side_effect = ["a", "a", "a", "a", "a", "b", "b", "b", "b", "b"]
-        conn.MARK_LENGTH = 5
-        conn._session = MagicMock()
-        conn._session.stdin.write = MagicMock()
-        conn._wrap_command = MagicMock()
-        conn._wrap_command.return_value = "cmd1"
-        conn._flush_stderr = MagicMock()
-        conn._windows = MagicMock()
-        conn._windows.return_value = True
-        conn._session.poll = MagicMock()
-        conn._session.poll.return_value = None
-        conn._timeout = MagicMock()
-        conn._poll_stdout = MagicMock()
-        conn._poll_stdout.poll = MagicMock()
-        conn._poll_stdout.poll.return_value = True
-        conn._session.stdout = MagicMock()
-        conn._session.stdout.readline = MagicMock()
-        conn._post_process = MagicMock()
-        conn._post_process.return_value = "test"
-        conn._session.stdout.readline.side_effect = iter(["aaaaa\n", "Hi\n", "0\n", "bbbbb\n"])
-        conn.get_option = MagicMock()
-        conn.get_option.return_value = 1
-        returncode = "a"
-        stdout = "b"
-        return (returncode, stdout, conn._flush_stderr)
-
-    def test_plugins_connection_aws_ssm_prepare_terminal(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.is_windows = MagicMock()
-        conn.is_windows.return_value = True
-
-    def test_plugins_connection_aws_ssm_wrap_command(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.is_windows = MagicMock()
-        conn.is_windows.return_value = True
-        return "windows1"
-
-    def test_plugins_connection_aws_ssm_post_process(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.is_windows = MagicMock()
-        conn.is_windows.return_value = True
-        conn.stdout = MagicMock()
-        returncode = 0
-        return returncode, conn.stdout
-
-    @patch("subprocess.Popen")
-    def test_plugins_connection_aws_ssm_flush_stderr(self, s_popen):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.poll_stderr = MagicMock()
-        conn.poll_stderr.register = MagicMock()
-        conn.stderr = None
-        s_popen.poll().return_value = 123
-        return conn.stderr
-
-    # XXX This isn't doing anything
-    # def test_plugins_connection_aws_ssm_get_url(self):
-    #     pc = PlayContext()
-    #     new_stdin = StringIO()
-    #     connection_loader.get('community.aws.aws_ssm', pc, new_stdin)
-    #     boto3 = MagicMock()
-    #     boto3.client('s3').return_value = MagicMock()
-    #     boto3.generate_presigned_url.return_value = MagicMock()
-    #     return (boto3.generate_presigned_url.return_value)
+        config = {"signature_version": "s3v4", "s3": {"addressing_style": options.get("s3_addressing_style")}}
+        loaded_aws_ssm._get_boto_client.assert_has_calls(
+            [
+                call("s3", endpoint_url=s3_endpoint_url, region_name=s3_region_name, config=config),
+                call("ssm", region_name=options.get("region"), config=config),
+            ]
+        )
+        assert loaded_aws_ssm.s3_manager.client == boto_clients["s3"]
+        assert loaded_aws_ssm._client == boto_clients["ssm"]
 
+    @pytest.mark.parametrize("path_exists", [True, False])
+    @patch("ansible_collections.community.aws.plugins.connection.aws_ssm.to_bytes")
     @patch("os.path.exists")
-    def test_plugins_connection_aws_ssm_put_file(self, mock_ospe):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn._connect = MagicMock()
-        conn._file_transport_command = MagicMock()
-        conn._file_transport_command.return_value = (0, "stdout", "stderr")
-        conn.put_file("/in/file", "/out/file")
-
-    def test_plugins_connection_aws_ssm_fetch_file(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn._connect = MagicMock()
-        conn._file_transport_command = MagicMock()
-        conn._file_transport_command.return_value = (0, "stdout", "stderr")
-        conn.fetch_file("/in/file", "/out/file")
-
-    @patch("subprocess.check_output")
-    @patch("boto3.client")
-    def test_plugins_connection_file_transport_command(self, boto_client, s_check_output):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.get_option = MagicMock()
-        conn.get_option.side_effect = ["1", "2", "3", "4", "5"]
-        conn._get_url = MagicMock()
-        conn._get_url.side_effect = ["url1", "url2"]
-        boto3 = MagicMock()
-        boto3.client("s3").return_value = MagicMock()
-        conn.get_option.return_value = 1
-        get_command = MagicMock()
-        put_command = MagicMock()
-        conn.exec_command = MagicMock()
-        conn.exec_command.return_value = (put_command, None, False)
-        conn.download_fileobj = MagicMock()
-        conn.exec_command(put_command, in_data=None, sudoable=False)
-        conn.exec_command(get_command, in_data=None, sudoable=False)
-
-    @patch("subprocess.check_output")
-    def test_plugins_connection_aws_ssm_close(self, s_check_output):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.instance_id = "i-12345"
-        conn._session_id = True
-        conn.get_option = MagicMock()
-        conn.get_option.side_effect = ["/abc", "pqr"]
-        conn._session = MagicMock()
-        conn._session.terminate = MagicMock()
-        conn._session.communicate = MagicMock()
-        conn._terminate_session = MagicMock()
-        conn._terminate_session.return_value = ""
-        conn._session_id = MagicMock()
-        conn._session_id.return_value = "a"
-        conn._client = MagicMock()
-        conn.close()
-
-    def test_generate_mark(self):
-        """Testing string generation"""
-        test_a = Connection.generate_mark()
-        test_b = Connection.generate_mark()
-
-        assert test_a != test_b
-        assert len(test_a) == Connection.MARK_LENGTH
-        assert len(test_b) == Connection.MARK_LENGTH
-
-    @pytest.mark.parametrize("is_windows", [False, True])
-    def test_generate_commands(self, is_windows):
-        """Testing command generation on both Windows and non-Windows systems"""
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.get_option = MagicMock()
-
-        conn.is_windows = is_windows
-
-        mock_s3_manager = MagicMock(spec=S3ClientManager)
-
-        mock_s3_manager.get_url.return_value = "https://test-url"
-        mock_s3_manager.generate_encryption_settings.return_value = (
-            {"ServerSideEncryption": "aws:kms"},
-            {"x-amz-server-side-encryption": "aws:kms"},
+    def test_plugins_connection_aws_ssm_put_file(self, mock_os_path_exists, mock_to_bytes, loaded_aws_ssm, path_exists):
+        mock_os_path_exists.return_value = path_exists
+
+        in_path = MagicMock()
+        out_path = MagicMock()
+        file_transport_result = MagicMock()
+        loaded_aws_ssm._file_transport_command = MagicMock(return_value=file_transport_result)
+        to_bytes_results = MagicMock()
+        mock_to_bytes.return_value = to_bytes_results
+        if path_exists:
+            assert loaded_aws_ssm.put_file(in_path, out_path) == file_transport_result
+            loaded_aws_ssm._file_transport_command.assert_called_once_with(in_path, out_path, "put")
+        else:
+            with pytest.raises(AnsibleFileNotFound) as exc_info:
+                loaded_aws_ssm.put_file(in_path, out_path)
+            str(exc_info.value).startswith("file or module does not exist: ")
+            loaded_aws_ssm._file_transport_command.assert_not_called()
+
+        mock_os_path_exists.assert_called_once_with(to_bytes_results)
+        mock_to_bytes.assert_called_once_with(in_path, errors="surrogate_or_strict")
+
+    def test_plugins_connection_aws_ssm_fetch_file(self, loaded_aws_ssm):
+        in_path = MagicMock()
+        out_path = MagicMock()
+        file_transport_result = MagicMock()
+        loaded_aws_ssm._file_transport_command = MagicMock(return_value=file_transport_result)
+
+        loaded_aws_ssm.fetch_file(in_path, out_path) == file_transport_result
+        loaded_aws_ssm._file_transport_command.assert_called_once_with(in_path, out_path, "get")
+
+    @pytest.mark.parametrize("ssm_action", ["get", "put", "another_action"])
+    @patch("ansible_collections.community.aws.plugins.connection.aws_ssm.to_bytes")
+    def test_plugins_connection_file_transport_command(self, mock_to_bytes, loaded_aws_ssm, ssm_action, tmp_path):
+        loaded_aws_ssm.is_windows = MagicMock()
+        loaded_aws_ssm.instance_id = MagicMock()
+        s3_path = MagicMock()
+        loaded_aws_ssm._escape_path = MagicMock(return_value=s3_path)
+        loaded_aws_ssm.test_options.update({"bucket_name": MagicMock()})
+        loaded_aws_ssm.s3_manager = MagicMock()
+        loaded_aws_ssm.s3_manager.client = MagicMock()
+
+        mock_to_bytes.side_effect = lambda x, **kwargs: x
+
+        command = MagicMock()
+        args = MagicMock()
+        loaded_aws_ssm.s3_manager.generate_host_commands = MagicMock(return_value=(command, args))
+
+        transport_result = MagicMock()
+        loaded_aws_ssm._exec_transport_commands = MagicMock(return_value=transport_result)
+
+        in_path = tmp_path / "in_file"
+        in_path.write_text("Some unit tests content", encoding="utf-8")
+        out_path = tmp_path / "out_file"
+
+        assert transport_result == loaded_aws_ssm._file_transport_command(in_path, out_path, ssm_action)
+
+        loaded_aws_ssm.s3_manager.client.delete_object.assert_called_once_with(
+            Bucket=loaded_aws_ssm.test_options["bucket_name"], Key=s3_path
         )
-        conn.s3_manager = mock_s3_manager
-
-        test_command_generation = conn._generate_commands(
-            "test_bucket",
-            "test/s3/path",
-            "test/in/path",
-            "test/out/path",
+        loaded_aws_ssm.s3_manager.generate_host_commands.assert_called_once_with(
+            loaded_aws_ssm.test_options["bucket_name"],
+            loaded_aws_ssm.test_options["bucket_sse_mode"],
+            loaded_aws_ssm.test_options["bucket_sse_kms_key_id"],
+            s3_path,
+            in_path,
+            out_path,
+            loaded_aws_ssm.is_windows,
+            ssm_action,
         )
-
-        # Check contents of generated command dictionaries
-        assert "command" in test_command_generation[0][0]
-        assert "method" in test_command_generation[0][0]
-        assert "headers" in test_command_generation[0][0]
-
-        if is_windows:
-            assert "Invoke-WebRequest" in test_command_generation[0][1]["command"]
-            assert test_command_generation[0][1]["method"] == "put"
-            # Two command dictionaries are generated for Windows
-            assert len(test_command_generation[0]) == 2
+        loaded_aws_ssm._exec_transport_commands.assert_called_once_with(in_path, out_path, command)
+        if ssm_action == "get":
+            loaded_aws_ssm.s3_manager.client.download_fileobj.assert_called_once_with(
+                loaded_aws_ssm.test_options["bucket_name"], s3_path, ANY
+            )
+            loaded_aws_ssm.s3_manager.client.upload_fileobj.assert_not_called()
         else:
-            put_cmd = test_command_generation[0][2]["command"]
-            assert "curl --request PUT" in put_cmd
-            assert "-H 'x-amz-server-side-encryption: aws:kms'" in put_cmd
-            assert "--upload-file 'test/in/path'" in put_cmd
-            assert "'https://test-url'" in put_cmd
-            assert test_command_generation[0][2]["method"] == "put"
-            # Three command dictionaries are generated on non-Windows systems
-            assert len(test_command_generation[0]) == 3
-
-        # Ensure data types of command object are as expected
-        assert isinstance(test_command_generation, tuple)
-        assert isinstance(test_command_generation[0], list)
-        assert isinstance(test_command_generation[0][0], dict)
-
+            loaded_aws_ssm.s3_manager.client.download_fileobj.assert_not_called()
+            loaded_aws_ssm.s3_manager.client.upload_fileobj.assert_called_once_with(
+                ANY, loaded_aws_ssm.test_options["bucket_name"], s3_path, ExtraArgs=args
+            )
+
+    @pytest.mark.parametrize("session_initialized", [True, False])
+    @pytest.mark.parametrize("client_initialized", [True, False])
+    @pytest.mark.parametrize("has_timeout", [True, False])
+    def test_plugins_connection_aws_ssm_close(
+        self, loaded_aws_ssm, session_initialized, client_initialized, has_timeout
+    ):
+        session_id = MagicMock()
+        if session_initialized:
+            loaded_aws_ssm._session_id = session_id
+        if client_initialized:
+            loaded_aws_ssm._client = MagicMock()
+        loaded_aws_ssm._has_timeout = has_timeout
+        loaded_aws_ssm._session = MagicMock()
+
+        loaded_aws_ssm.close()
+        if not session_initialized:
+            loaded_aws_ssm._session.terminate.assert_not_called()
+            loaded_aws_ssm._session.communicate.assert_not_called()
+            if loaded_aws_ssm._client:
+                loaded_aws_ssm._client.terminate_session.assert_not_called()
+        else:
+            if has_timeout:
+                loaded_aws_ssm._session.terminate.assert_called_once_with()
+                loaded_aws_ssm._session.communicate.assert_not_called()
+            else:
+                loaded_aws_ssm._session.communicate.assert_called_once_with(b"\nexit\n")
+                loaded_aws_ssm._session.terminate.assert_not_called()
+            if loaded_aws_ssm._client:
+                loaded_aws_ssm._client.terminate_session.assert_called_once_with(SessionId=session_id)
+            assert loaded_aws_ssm._session_id == ""
+
+    #     def test_generate_mark(self):
+    #         """Testing string generation"""
+    #         test_a = Connection.generate_mark()
+    #         test_b = Connection.generate_mark()
+
+    #         assert test_a != test_b
+    #         assert len(test_a) == Connection.MARK_LENGTH
+    #         assert len(test_b) == Connection.MARK_LENGTH
+
+    @pytest.mark.parametrize("level", ["invalid value", 5, -1])
+    @patch("ansible_collections.community.aws.plugins.connection.aws_ssm.display")
+    def test_verbosity_diplay_invalid_level(self, mock_display, loaded_aws_ssm, level):
+        """Testing verbosity levels"""
+        # Test exception is raised when verbosity level is not an accepted value
+        with pytest.raises(AnsibleError) as exc_info:
+            loaded_aws_ssm.verbosity_display(level, "unit testing connection aws_ssm plugin")
+        assert str(exc_info.value) == (f"Invalid verbosity level: {level}")
+        for method in ("v", "vv", "vvv", "vvvv"):
+            getattr(mock_display, method).assert_not_called()
+
+    @pytest.mark.parametrize("host", [None, "test-host"])
     @pytest.mark.parametrize(
-        "message,level,method",
+        "level,method",
         [
-            ("test message 1", 1, "v"),
-            ("test message 2", 2, "vv"),
-            ("test message 3", 3, "vvv"),
-            ("test message 4", 4, "vvvv"),
+            (1, "v"),
+            (2, "vv"),
+            (3, "vvv"),
+            (4, "vvvv"),
         ],
     )
-    def test_verbosity_diplay(self, message, level, method):
+    @patch("ansible_collections.community.aws.plugins.connection.aws_ssm.display")
+    def test_verbosity_diplay(self, mock_display, loaded_aws_ssm, host, level, method):
         """Testing verbosity levels"""
-        play_context = MagicMock()
-        play_context.shell = "sh"
-        conn = Connection(play_context)
-        conn.host = "test-host"  # Test with host set
-
-        with patch("ansible_collections.community.aws.plugins.connection.aws_ssm.display") as mock_display:
-            conn.verbosity_display(level, message)
-            # Verify the correct display method was called with expected args
-            mock_method = getattr(mock_display, method)
-            mock_method.assert_called_once_with(message, host="test-host")
-
-            # Test without host set
-            conn.host = None
-            conn.verbosity_display(1, "no host message")
-            mock_display.v.assert_called_with("no host message")
-
-            # Test exception is raised when verbosity level is not an accepted value
-            with pytest.raises(AnsibleError):
-                conn.verbosity_display("invalid value", "test message")
-
-    def test_poll_verbosity(self):
+        loaded_aws_ssm.host = host
+        message = "unit testing connection aws_ssm plugin"
+        loaded_aws_ssm.verbosity_display(level, message)
+        # Verify the correct display method was called with expected args
+        args = {}
+        if host:
+            args["host"] = host
+        getattr(mock_display, method).assert_called_once_with(message, **args)
+
+    def test_poll_verbosity(self, loaded_aws_ssm):
         """Test poll method verbosity display"""
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        conn._session = MagicMock()
-        conn._session.poll.return_value = None
-        conn.get_option = MagicMock(return_value=10)  # ssm_timeout
-        conn.poll_stdout = MagicMock()
-        conn.instance_id = "i-1234567890"
-        conn.host = conn.instance_id
-
-        with patch("time.time", return_value=100), patch.object(conn, "verbosity_display") as mock_display:
-            poll_gen = conn.poll("TEST", "test command")
+
+        loaded_aws_ssm._session = MagicMock()
+        loaded_aws_ssm._session.poll.return_value = None
+        loaded_aws_ssm.get_option = MagicMock(return_value=10)  # ssm_timeout
+        loaded_aws_ssm.poll_stdout = MagicMock()
+        loaded_aws_ssm.instance_id = "i-1234567890"
+        loaded_aws_ssm.host = loaded_aws_ssm.instance_id
+
+        with patch("time.time", return_value=100), patch.object(loaded_aws_ssm, "verbosity_display") as mock_display:
+            poll_gen = loaded_aws_ssm.poll("TEST", "test command")
             # Advance generator twice to trigger the verbosity message
             next(poll_gen)
             next(poll_gen)
@@ -367,116 +274,140 @@ class TestS3ClientManager:
     Tests for the S3ClientManager class
     """
 
-    @patch(
-        "ansible_collections.community.aws.plugins.connection.aws_ssm.S3ClientManager.get_s3_client",
-        return_value="mocked_s3_client",
-    )
-    def test_initialize_client(self, mock_get_s3_client):
-        """
-        Test initialize_client()
-        """
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        s3_client_manager = S3ClientManager(connection=conn)
+    def create_object(self):
+        self.client = MagicMock()
+        return S3ClientManager(self.client)
 
-        s3_client_manager.initialize_client(
-            region_name="us-east-2", endpoint_url="https://mock-endpoint", profile_name="test-profile"
+    @pytest.mark.parametrize("method", ["get", "put", "fake"])
+    @pytest.mark.parametrize("is_windows", [False, True])
+    @patch("ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.generate_encryption_settings")
+    def test_generate_host_commands(self, m_generate_encryption_settings, method, is_windows):
+        """Testing command generation on both Windows and non-Windows systems"""
+        s3_client_manager = self.create_object()
+
+        s3_client_manager.get_url = MagicMock()
+        s3_client_manager.get_url.return_value = "https://test-url"
+        encryption_headers = {"ServerSideEncryption": "aws:kms"}
+        encryption_args = MagicMock()
+        m_generate_encryption_settings.return_value = (encryption_args, encryption_headers)
+
+        bucket_sse_mode = MagicMock()
+        bucket_sse_kms_key_id = MagicMock()
+        s3_path = MagicMock()
+        bucket_name = MagicMock()
+
+        test_command_generation, put_args = s3_client_manager.generate_host_commands(
+            bucket_name=bucket_name,
+            bucket_sse_mode=bucket_sse_mode,
+            bucket_sse_kms_key_id=bucket_sse_kms_key_id,
+            s3_path=s3_path,
+            in_path="test/in/path",
+            out_path="test/out/path",
+            is_windows=is_windows,
+            method=method,
         )
 
-        assert mock_get_s3_client.call_count == 1
+        if method not in ("get", "put"):
+            assert put_args is None
+            test_command_generation is None
+        else:
+            assert isinstance(test_command_generation, str)
+            print(m_generate_encryption_settings.mock_calls)
+            if method == "get":
+                m_generate_encryption_settings.assert_called_once_with(bucket_sse_mode, bucket_sse_kms_key_id)
+                if is_windows:
+                    assert test_command_generation.startswith("Invoke-WebRequest -Method PUT -Headers @")
+                    assert test_command_generation.endswith(
+                        "-InFile 'test/in/path' -Uri 'https://test-url' -UseBasicParsing"
+                    )
+                else:
+                    assert test_command_generation.startswith("curl --request PUT ")
+                    assert test_command_generation.endswith("--upload-file 'test/in/path' 'https://test-url'")
+                assert put_args == encryption_args
+                s3_client_manager.get_url.assert_called_once_with(
+                    "put_object", bucket_name, s3_path, "PUT", extra_args=encryption_args
+                )
+            elif method == "put":
+                m_generate_encryption_settings.assert_not_called()
+                if is_windows:
+                    assert "Invoke-WebRequest 'https://test-url' -OutFile 'test/out/path'" == test_command_generation
+                else:
+                    assert "curl -o 'test/out/path' 'https://test-url';touch 'test/out/path'" == test_command_generation
+                assert put_args is None
+                s3_client_manager.get_url.assert_called_once_with("get_object", bucket_name, s3_path, "GET")
+
+    @pytest.mark.parametrize("bucket_endpoint_url", [None, "bucket_endurl_test"])
+    @pytest.mark.parametrize(
+        "region_name,bucket_region",
+        [
+            (None, "eu-west-2"),
+            ("us-east-1", "eu-west-2"),
+            ("eu-east-1", "eu-west-2"),
+            ("eu-west", "eu-west-2"),
+        ],
+    )
+    @patch("ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.S3ClientManager._get_s3_client")
+    def test_get_bucket_endpoint(self, mock__get_s3_client, bucket_endpoint_url, region_name, bucket_region):
+        tmp_s3_1 = MagicMock()
+        tmp_s3_2 = MagicMock()
 
-        mock_get_s3_client.assert_called_once_with(
-            region_name="us-east-2",
-            endpoint_url="https://mock-endpoint",
-            profile_name="test-profile",
+        tmp_s3_1.head_bucket = MagicMock(
+            return_value={"ResponseMetadata": {"HTTPHeaders": {"x-amz-bucket-region": bucket_region}}}
         )
-
-        assert s3_client_manager._s3_client is not None
-        assert s3_client_manager._s3_client == "mocked_s3_client"
-
-    def test_get_bucket_endpoint(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        # Mock get_option to return expected values
-        def mock_get_option(key):
-            options = {
-                "region": "us-east-2",
-                "profile": "test-profile",
-                "bucket_name": "my-bucket",
-                "bucket_endpoint_url": None,
-            }
-            return options.get(key)
-
-        conn.get_option = MagicMock(side_effect=mock_get_option)
-
-        s3_manager = S3ClientManager(connection=conn)
-
-        mock_tmp_client = MagicMock()
-        mock_tmp_client.head_bucket.return_value = {
-            "ResponseMetadata": {"HTTPHeaders": {"x-amz-bucket-region": "us-east-2"}}
-        }
-
-        mock_region_client = MagicMock()
-        mock_region_client.meta.endpoint_url = "https://s3.us-east-2.amazonaws.com"
-        mock_region_client.meta.region_name = "us-east-2"
-
-        s3_manager.get_s3_client = MagicMock(side_effect=[mock_tmp_client, mock_region_client])
-
-        endpoint, region = s3_manager.get_bucket_endpoint()
-
-        assert endpoint == "https://s3.us-east-2.amazonaws.com"
-        assert region == "us-east-2"
-
-    @patch("boto3.session.Session")
-    def test_get_s3_client(self, mock_session_cls):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        # Mock get_option to return expected values
-        def mock_get_option(key):
-            options = {
-                "access_key_id": "dummy_key",
-                "secret_access_key": "dummy_secret",
-                "session_token": "dummy_token",
-                "s3_addressing_style": "path",
-            }
-            return options.get(key, None)
-
-        conn.get_option = MagicMock(side_effect=mock_get_option)
-
-        s3_manager = S3ClientManager(connection=conn)
-
-        mock_session = MagicMock()
-        mock_client = "mock_client"
-        mock_session.client.return_value = mock_client
-        mock_session_cls.return_value = mock_session
-
-        client = s3_manager.get_s3_client(
-            region_name="us-east-2", profile_name="test-profile", endpoint_url="http://example.com"
+        tmp_s3_2.head_bucket = MagicMock()
+
+        mock__get_s3_client.side_effect = [tmp_s3_1, tmp_s3_2]
+
+        bucket_name = MagicMock()
+        access_key_id = MagicMock()
+        secret_key_id = MagicMock()
+        session_token = MagicMock()
+        profile_name = MagicMock()
+
+        endpoint_url, region = S3ClientManager.get_bucket_endpoint(
+            bucket_name=bucket_name,
+            bucket_endpoint_url=bucket_endpoint_url,
+            access_key_id=access_key_id,
+            secret_key_id=secret_key_id,
+            session_token=session_token,
+            region_name=region_name,
+            profile_name=profile_name,
         )
 
-        assert client == mock_client
+        tmp_s3_1.head_bucket.assert_called_once_with(Bucket=(bucket_name))
+        test_region_name = region_name or "us-east-1"
+        test_bucket_region = bucket_region or "us-east-1"
+        if bucket_endpoint_url:
+            assert bucket_endpoint_url == endpoint_url
+            assert test_bucket_region == region
+            mock__get_s3_client.assert_called_once_with(
+                access_key_id, secret_key_id, session_token, test_region_name, profile_name
+            )
+        elif test_bucket_region == test_region_name:
+            assert endpoint_url == tmp_s3_1.meta.endpoint_url
+            assert region == tmp_s3_1.meta.region_name
+            mock__get_s3_client.assert_called_once_with(
+                access_key_id, secret_key_id, session_token, test_region_name, profile_name
+            )
+        else:
+            assert endpoint_url == tmp_s3_2.meta.endpoint_url
+            assert region == tmp_s3_2.meta.region_name
+            mock__get_s3_client.assert_has_calls(
+                [
+                    call(access_key_id, secret_key_id, session_token, test_region_name, profile_name),
+                    call(access_key_id, secret_key_id, session_token, test_bucket_region, profile_name),
+                ]
+            )
 
     def test_get_url_no_extra_args(self):
         """
         Test get_url() without extra_args
         """
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        s3_manager = S3ClientManager(connection=conn)
-        mock_s3_client = MagicMock()
-        s3_manager._s3_client = mock_s3_client
+        s3_client_manager = self.create_object()
         # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/generate_presigned_url.html
-        mock_s3_client.generate_presigned_url.return_value = "http://test-url-extra"
+        self.client.generate_presigned_url.return_value = "http://test-url-extra"
 
-        result = s3_manager.get_url(
+        result = s3_client_manager.get_url(
             client_method="put_object",
             bucket_name="test_bucket",
             out_path="test/path",
@@ -485,7 +416,7 @@ def test_get_url_no_extra_args(self):
 
         expected_params = {"Bucket": "test_bucket", "Key": "test/path"}
 
-        mock_s3_client.generate_presigned_url.assert_called_once_with(
+        self.client.generate_presigned_url.assert_called_once_with(
             "put_object", Params=expected_params, ExpiresIn=3600, HttpMethod="PUT"
         )
         assert result == "http://test-url-extra"
@@ -494,18 +425,12 @@ def test_get_url_extra_args(self):
         """
         Test get_url() with extra_args
         """
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        s3_manager = S3ClientManager(connection=conn)
-        mock_s3_client = MagicMock()
-        s3_manager._s3_client = mock_s3_client
+        s3_client_manager = self.create_object()
         # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/generate_presigned_url.html
-        mock_s3_client.generate_presigned_url.return_value = "http://test-url-extra"
+        self.client.generate_presigned_url.return_value = "http://test-url-extra"
         extra_args = {"ACL": "public-read", "ContentType": "text/plain"}
 
-        result = s3_manager.get_url(
+        result = s3_client_manager.get_url(
             client_method="put_object",
             bucket_name="test_bucket",
             out_path="test/path",
@@ -516,41 +441,37 @@ def test_get_url_extra_args(self):
         expected_params = {"Bucket": "test_bucket", "Key": "test/path"}
         expected_params.update(extra_args)
 
-        mock_s3_client.generate_presigned_url.assert_called_once_with(
+        self.client.generate_presigned_url.assert_called_once_with(
             "put_object", Params=expected_params, ExpiresIn=3600, HttpMethod="PUT"
         )
         assert result == "http://test-url-extra"
 
-    def test_generate_encryption_settings(self):
-        """
-        Test generate_encryption_settings()
-        """
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-
-        # Mock get_option to return expected values
-        def mock_get_option(key):
-            options = {
-                "profile": "test-profile",
-                "region": "us-east-1",
-                "bucket_sse_mode": "aws:kms",
-                "bucket_sse_kms_key_id": "my-kms-key-id",
-            }
-            return options.get(key, None)
-
-        conn.get_option = MagicMock(side_effect=mock_get_option)
-
-        s3_manager = S3ClientManager(connection=conn)
-        put_args, put_headers = s3_manager.generate_encryption_settings()
-
-        expected_put_args = {"ServerSideEncryption": "aws:kms", "SSEKMSKeyId": "my-kms-key-id"}
-        expected_put_headers = {
-            "x-amz-server-side-encryption": "aws:kms",
-            "x-amz-server-side-encryption-aws-kms-key-id": "my-kms-key-id",
-        }
 
-        assert put_args == expected_put_args
-        assert put_headers == expected_put_headers
-        conn.get_option.assert_any_call("bucket_sse_mode")
-        conn.get_option.assert_any_call("bucket_sse_kms_key_id")
+@pytest.mark.parametrize(
+    "bucket_sse_mode,bucket_sse_kms_key_id,args,headers",
+    [
+        (None, "We do not care about this", {}, {}),
+        (
+            "sse_no_kms",
+            "sse_key_id",
+            {"ServerSideEncryption": "sse_no_kms"},
+            {"x-amz-server-side-encryption": "sse_no_kms"},
+        ),
+        ("aws:kms", "", {"ServerSideEncryption": "aws:kms"}, {"x-amz-server-side-encryption": "aws:kms"}),
+        ("aws:kms", None, {"ServerSideEncryption": "aws:kms"}, {"x-amz-server-side-encryption": "aws:kms"}),
+        (
+            "aws:kms",
+            "test_kms_id",
+            {"ServerSideEncryption": "aws:kms", "SSEKMSKeyId": "test_kms_id"},
+            {"x-amz-server-side-encryption": "aws:kms", "x-amz-server-side-encryption-aws-kms-key-id": "test_kms_id"},
+        ),
+    ],
+)
+def test_generate_encryption_settings(bucket_sse_mode, bucket_sse_kms_key_id, args, headers):
+    """
+    Test generate_encryption_settings()
+    """
+
+    r_args, r_headers = generate_encryption_settings(bucket_sse_mode, bucket_sse_kms_key_id)
+    assert r_args == args
+    assert r_headers == headers


Reviews:
user: login: GomathiselviS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T14:05:01Z
user: login: GomathiselviS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:24:39Z
user: login: GomathiselviS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:35:14Z
user: login: beeankha
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T19:39:31Z
user: login: GomathiselviS
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T20:28:22Z
user: login: abikouo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T08:57:23Z
user: login: abikouo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:23:27Z
user: login: abikouo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:41:36Z
user: login: beeankha
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:49:56Z
user: login: GomathiselviS
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T16:21:29Z

Review comments:
diff_hunk: @@ -888,153 +876,26 @@ def _flush_stderr(self, session_process) -> str:
 
         return stderr
 
-    def _get_boto_client(self, service, region_name=None, profile_name=None, endpoint_url=None):
-        """Gets a boto3 client based on the STS token"""
-
-        aws_access_key_id = self.get_option("access_key_id")
-        aws_secret_access_key = self.get_option("secret_access_key")
-        aws_session_token = self.get_option("session_token")
-
-        session_args = dict(
-            aws_access_key_id=aws_access_key_id,
-            aws_secret_access_key=aws_secret_access_key,
-            aws_session_token=aws_session_token,
-            region_name=region_name,
-        )
-        if profile_name:
-            session_args["profile_name"] = profile_name
-        session = boto3.session.Session(**session_args)
-
-        client = session.client(
-            service,
-            endpoint_url=endpoint_url,
-            config=Config(
-                signature_version="s3v4",
-                s3={"addressing_style": self.get_option("s3_addressing_style")},
-            ),
-        )
-        return client
-
     def _escape_path(self, path: str) -> str:
         return path.replace("\\", "/")
 
-    def _generate_commands(
-        self,
-        bucket_name: str,
-        s3_path: str,
-        in_path: str,
-        out_path: str,
-    ) -> Tuple[List[Dict], dict]:
-        """
-        Generate commands for the specified bucket, S3 path, input path, and output path.
-
-        :param bucket_name: The name of the S3 bucket used for file transfers.
-        :param s3_path: The S3 path to the file to be sent.
-        :param in_path: Input path
-        :param out_path: Output path
-        :param method: The request method to use for the command (can be "get" or "put").
-
-        :returns: A tuple containing a list of command dictionaries along with any ``put_args`` dictionaries.
-        """
-
-        put_args, put_headers = self.s3_manager.generate_encryption_settings()
-        commands = []
-
-        put_url = self.s3_manager.get_url("put_object", bucket_name, s3_path, "PUT", extra_args=put_args)
-        get_url = self.s3_manager.get_url("get_object", bucket_name, s3_path, "GET")
-
-        if self.is_windows:
-            put_command_headers = "; ".join([f"'{h}' = '{v}'" for h, v in put_headers.items()])
-            commands.append({
-                "command":
-                (
-                    "Invoke-WebRequest "
-                    f"'{get_url}' "
-                    f"-OutFile '{out_path}'"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are get_commands
-                "method": "get",
-                "headers": {},
-            })  # fmt: skip
-            commands.append({
-                "command":
-                (
-                    "Invoke-WebRequest -Method PUT "
-                    # @{'key' = 'value'; 'key2' = 'value2'}
-                    f"-Headers @{{{put_command_headers}}} "
-                    f"-InFile '{in_path}' "
-                    f"-Uri '{put_url}' "
-                    f"-UseBasicParsing"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are put_commands
-                "method": "put",
-                "headers": put_headers,
-            })  # fmt: skip
-        else:
-            put_command_headers = " ".join([f"-H '{h}: {v}'" for h, v in put_headers.items()])
-            commands.append({
-                "command":
-                (
-                    "curl "
-                    f"-o '{out_path}' "
-                    f"'{get_url}'"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are get_commands
-                "method": "get",
-                "headers": {},
-            })  # fmt: skip
-            # Due to https://github.com/curl/curl/issues/183 earlier
-            # versions of curl did not create the output file, when the
-            # response was empty. Although this issue was fixed in 2015,
-            # some actively maintained operating systems still use older
-            # versions of it (e.g. CentOS 7)
-            commands.append({
-                "command":
-                (
-                    "touch "
-                    f"'{out_path}'"
-                ),
-                "method": "get",
-                "headers": {},
-            })  # fmt: skip
-            commands.append({
-                "command":
-                (
-                    "curl --request PUT "
-                    f"{put_command_headers} "
-                    f"--upload-file '{in_path}' "
-                    f"'{put_url}'"
-                ),
-                # The "method" key indicates to _file_transport_command which commands are put_commands
-                "method": "put",
-                "headers": put_headers,
-            })  # fmt: skip
-
-        return commands, put_args
-
-    def _exec_transport_commands(self, in_path: str, out_path: str, commands: List[dict]) -> CommandResult:
+    def _exec_transport_commands(self, in_path: str, out_path: str, command: dict) -> CommandResult:
         """
         Execute the provided transport commands.
path: plugins/connection/aws_ssm.py
user: login: GomathiselviS
body: ```suggestion
        Execute the provided transport command.
```
created_at: 2025-04-04T13:43:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +117,74 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[List[Dict], dict]:
path: plugins/plugin_utils/s3clientmanager.py
user: login: GomathiselviS
body: ```suggestion
    ) -> Tuple[str, Optional[dict]]:
```
created_at: 2025-04-04T13:49:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +117,74 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[List[Dict], dict]:
+        """
+        Generate commands for the specified bucket, S3 path, input path, and output path.
+
+        :param bucket_name: The name of the S3 bucket used for file transfers.
+        :param s3_path: The S3 path to the file to be sent.
+        :param in_path: Input path
+        :param out_path: Output path
+        :param method: The request method to use for the command (can be "get" or "put").
+
+        :returns: A tuple containing a list of command dictionaries along with any ``put_args`` dictionaries.
path: plugins/plugin_utils/s3clientmanager.py
user: login: GomathiselviS
body: ```suggestion
        :returns: A tuple containing a shell command string and optional extra arguments for the PUT request.
```
created_at: 2025-04-04T13:49:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,160 +26,58 @@ class TestConnectionBaseClass:
         "ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.S3ClientManager.get_bucket_endpoint",
         return_value=("fake-s3-endpoint", "fake-region"),
     )
-    @patch(
-        "ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.S3ClientManager.get_s3_client",
-        return_value=MagicMock(),
-    )
-    def test_init_clients(self, mock_get_s3_client, mock_get_bucket_endpoint):
+    def test_init_clients(self, mock_get_bucket_endpoint):
         pc = PlayContext()
         new_stdin = StringIO()
         conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
 
-        def mock_get_option(key):
-            options = {
-                "profile": "test-profile",
-                "region": "us-east-1",
-            }
-            return options.get(key, None)
-
-        conn.get_option = MagicMock(side_effect=mock_get_option)
-
-        # Mock the _initialize_ssm_client and _initialize_s3_client methods
-        conn._initialize_ssm_client = MagicMock()
-
-        conn._init_clients()
-
-        # Assert that _initialize_ssm_client was called once
-        conn._initialize_ssm_client.assert_called_once_with("us-east-1", "test-profile")
+        boto_clients = {"ssm": MagicMock(), "s3": MagicMock()}
 
-        # Assert that get_bucket_endpoint was called once
-        mock_get_bucket_endpoint.assert_called_once()
+        def mock_get_boto_client(service, *args, **kwargs):
+            return boto_clients.get(service)
 
-        # Assert that get_s3_client was called with appropriate arguments
-        mock_get_s3_client.assert_called_once()
+        conn._get_boto_client = MagicMock(side_effect=mock_get_boto_client)
 
-        # Assert that self._s3_client is not None
-        assert conn._s3_client is not None
-        assert conn._s3_client is conn.s3_manager._s3_client
-
-    @patch("boto3.client")
-    def test_initialize_ssm_client(self, mock_boto3_client):
-        """
-        Test for the _initialize_ssm_client method to ensure the SSM client is initialized correctly.
-        """
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
+        options = {
+            "s3_addressing_style": MagicMock(),
+            "bucket_endpoint_url": MagicMock(),
+            "bucket_name": MagicMock(),
+            "access_key_id": MagicMock(),
+            "secret_access_key": MagicMock(),
+            "session_token": MagicMock(),
+            "region": MagicMock(),
+            "profile": MagicMock(),
+        }
 
-        test_region_name = "us-west-2"
-        test_profile_name = "test-profile"
+        def mock_get_option(name):
+            return options.get(name)
 
-        # Mock the _get_boto_client method to return a mock client
-        conn._get_boto_client = MagicMock(return_value=mock_boto3_client)
+        conn.get_option = MagicMock(side_effect=mock_get_option)
+        s3_endpoint_url, s3_region_name = MagicMock(), MagicMock()
+        mock_get_bucket_endpoint.return_value = (s3_endpoint_url, s3_region_name)
 
-        conn._initialize_ssm_client(test_region_name, test_profile_name)
+        conn._init_clients()
 
-        conn._get_boto_client.assert_called_once_with(
-            "ssm",
-            region_name=test_region_name,
-            profile_name=test_profile_name,
+        # Validate results
+        mock_get_bucket_endpoint.assert_called_once_with(
+            bucket_name=options.get("bucket_name"),
+            bucket_endpoint_url=options.get("bucket_endpoint_url"),
+            access_key_id=options.get("access_key_id"),
+            secret_key_id=options.get("secret_access_key"),
+            session_token=options.get("session_token"),
+            region_name=options.get("region"),
+            profile_name=options.get("profile"),
         )
 
-        assert conn._client is mock_boto3_client
-
-    @patch("os.path.exists")
-    @patch("subprocess.Popen")
-    @patch("select.poll")
-    @patch("boto3.client")
-    def test_plugins_connection_aws_ssm_start_session(self, boto_client, s_poll, s_popen, mock_ospe):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.get_option = MagicMock()
-        conn.get_option.side_effect = ["i1234", "executable", "abcd", "i1234"]
-        conn.host = "abc"
-        mock_ospe.return_value = True
-        boto3 = MagicMock()
-        boto3.client("ssm").return_value = MagicMock()
-        conn.start_session = MagicMock()
-        conn._session_id = MagicMock()
-        conn._session_id.return_value = "s1"
-        s_popen.return_value.stdin.write = MagicMock()
-        s_poll.return_value = MagicMock()
-        s_poll.return_value.register = MagicMock()
-        s_popen.return_value.poll = MagicMock()
-        s_popen.return_value.poll.return_value = None
-        conn._stdin_readline = MagicMock()
-        conn._stdin_readline.return_value = "abc123"
-        conn.SESSION_START = "abc"
-        conn.start_session()
-
-    @patch("random.choice")
-    def test_plugins_connection_aws_ssm_exec_command(self, r_choice):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        r_choice.side_effect = ["a", "a", "a", "a", "a", "b", "b", "b", "b", "b"]
-        conn.MARK_LENGTH = 5
-        conn._session = MagicMock()
-        conn._session.stdin.write = MagicMock()
-        conn._wrap_command = MagicMock()
-        conn._wrap_command.return_value = "cmd1"
-        conn._flush_stderr = MagicMock()
-        conn._windows = MagicMock()
-        conn._windows.return_value = True
-        conn._session.poll = MagicMock()
-        conn._session.poll.return_value = None
-        conn._timeout = MagicMock()
-        conn._poll_stdout = MagicMock()
-        conn._poll_stdout.poll = MagicMock()
-        conn._poll_stdout.poll.return_value = True
-        conn._session.stdout = MagicMock()
-        conn._session.stdout.readline = MagicMock()
-        conn._post_process = MagicMock()
-        conn._post_process.return_value = "test"
-        conn._session.stdout.readline.side_effect = iter(["aaaaa\n", "Hi\n", "0\n", "bbbbb\n"])
-        conn.get_option = MagicMock()
-        conn.get_option.return_value = 1
-        returncode = "a"
-        stdout = "b"
-        return (returncode, stdout, conn._flush_stderr)
-
-    def test_plugins_connection_aws_ssm_prepare_terminal(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.is_windows = MagicMock()
-        conn.is_windows.return_value = True
-
-    def test_plugins_connection_aws_ssm_wrap_command(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.is_windows = MagicMock()
-        conn.is_windows.return_value = True
-        return "windows1"
-
-    def test_plugins_connection_aws_ssm_post_process(self):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.is_windows = MagicMock()
-        conn.is_windows.return_value = True
-        conn.stdout = MagicMock()
-        returncode = 0
-        return returncode, conn.stdout
-
-    @patch("subprocess.Popen")
-    def test_plugins_connection_aws_ssm_flush_stderr(self, s_popen):
-        pc = PlayContext()
-        new_stdin = StringIO()
-        conn = connection_loader.get("community.aws.aws_ssm", pc, new_stdin)
-        conn.poll_stderr = MagicMock()
-        conn.poll_stderr.register = MagicMock()
-        conn.stderr = None
-        s_popen.poll().return_value = 123
-        return conn.stderr
+        config = {"signature_version": "s3v4", "s3": {"addressing_style": options.get("s3_addressing_style")}}
+        conn._get_boto_client.assert_has_calls(
+            [
+                call("s3", endpoint_url=s3_endpoint_url, region_name=s3_region_name, config=config),
+                call("ssm", region_name=options.get("region"), config=config),
+            ]
+        )
+        assert conn.s3_manager.client == boto_clients["s3"]
+        assert conn._client == boto_clients["ssm"]
 
     # XXX This isn't doing anything
path: tests/unit/plugins/connection/aws_ssm/test_aws_ssm.py
user: login: GomathiselviS
body: Can we remove this test?
created_at: 2025-04-04T13:53:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,160 +26,58 @@ class TestConnectionBaseClass:
         "ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.S3ClientManager.get_bucket_endpoint",
         return_value=("fake-s3-endpoint", "fake-region"),
     )
-    @patch(
-        "ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager.S3ClientManager.get_s3_client",
-        return_value=MagicMock(),
-    )
-    def test_init_clients(self, mock_get_s3_client, mock_get_bucket_endpoint):
+    def test_init_clients(self, mock_get_bucket_endpoint):
         pc = PlayContext()
path: tests/unit/plugins/connection/aws_ssm/test_aws_ssm.py
user: login: GomathiselviS
body:  PlayContext, StringIO(), and connection_loader.get(...) are repeated a lot. As part of refactoring, can we extract the repeated setup logic into a test fixture or a helper function, so that it can be reused across different tests?
created_at: 2025-04-04T14:04:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from typing import Any
+from typing import Dict
+from typing import Optional
+
+try:
+    import boto3
+    from botocore.client import Config
+except ImportError:
+    pass
+
+
+class AwsConnectionPluginBase:
+    def __init__(self) -> None:
+        pass
+
+    def _get_boto_client(
+        self,
+        service: str,
+        region_name: Optional[str] = None,
+        endpoint_url: Optional[str] = None,
+        config: Optional[Dict[str, Any]] = None,
+    ):
path: plugins/plugin_utils/base.py
user: login: GomathiselviS
body: Should the return type be `BaseClient` ?
created_at: 2025-04-07T13:24:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +116,74 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[str, Optional[Dict[str, str]]]:
+        """
+        Generate commands for the specified bucket, S3 path, input path, and output path.
+
+        :param bucket_name: The name of the S3 bucket used for file transfers.
+        :param s3_path: The S3 path to the file to be sent.
+        :param in_path: Input path
+        :param out_path: Output path
+        :param method: The request method to use for the command (can be "get" or "put").
+
+        :returns: A tuple containing a shell command string and optional extra arguments for the PUT request.
+        """
+
+        command = None
+        url = None
+        put_args = None
+        if method == "get":
path: plugins/plugin_utils/s3clientmanager.py
user: login: GomathiselviS
body: Is this right? I am little confused as you are using put_args and "PUT" method under this. Would you mind using the correct variable names and method values to avoid the confusion?
created_at: 2025-04-07T13:31:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +116,74 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[str, Optional[Dict[str, str]]]:
+        """
+        Generate commands for the specified bucket, S3 path, input path, and output path.
+
+        :param bucket_name: The name of the S3 bucket used for file transfers.
+        :param s3_path: The S3 path to the file to be sent.
+        :param in_path: Input path
+        :param out_path: Output path
+        :param method: The request method to use for the command (can be "get" or "put").
+
+        :returns: A tuple containing a shell command string and optional extra arguments for the PUT request.
+        """
+
+        command = None
+        url = None
+        put_args = None
+        if method == "get":
path: plugins/plugin_utils/s3clientmanager.py
user: login: beeankha
body: Yes, this is confusing at first glance but it's correct (I was also puzzled by this when working on #2248). @abikouo please correct me if I'm wrong but from my understanding, the reason for this apparent contradiction is that the code is handling file transfers between a local machine and a remote AWS instance via S3 as an intermediary. The [`ssm_action`](https://github.com/ansible-collections/community.aws/pull/2275/files#diff-8000d7fb9262e11565b61882567d729fe0958cd9b1a0105683d5bbe0e5b4e585L1068-R940) refers to what the user/caller wants to do (`get` a file from the remote instance or `put` a file to the remote instance), while the `method` in the commands refers to what the remote instance needs to do. 

@GomathiselviS @abikouo  do you think it might be helpful to add some comments to clarify why the specified methods seem to be the opposite of what they should be? 


created_at: 2025-04-07T19:39:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +116,74 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[str, Optional[Dict[str, str]]]:
+        """
+        Generate commands for the specified bucket, S3 path, input path, and output path.
+
+        :param bucket_name: The name of the S3 bucket used for file transfers.
+        :param s3_path: The S3 path to the file to be sent.
+        :param in_path: Input path
+        :param out_path: Output path
+        :param method: The request method to use for the command (can be "get" or "put").
+
+        :returns: A tuple containing a shell command string and optional extra arguments for the PUT request.
+        """
+
+        command = None
+        url = None
+        put_args = None
+        if method == "get":
path: plugins/plugin_utils/s3clientmanager.py
user: login: GomathiselviS
body: Thanks for the clarification, @beeankha. Yes, comments would be helpful for posterity.
created_at: 2025-04-07T20:28:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +116,74 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[str, Optional[Dict[str, str]]]:
+        """
+        Generate commands for the specified bucket, S3 path, input path, and output path.
+
+        :param bucket_name: The name of the S3 bucket used for file transfers.
+        :param s3_path: The S3 path to the file to be sent.
+        :param in_path: Input path
+        :param out_path: Output path
+        :param method: The request method to use for the command (can be "get" or "put").
+
+        :returns: A tuple containing a shell command string and optional extra arguments for the PUT request.
+        """
+
+        command = None
+        url = None
+        put_args = None
+        if method == "get":
path: plugins/plugin_utils/s3clientmanager.py
user: login: abikouo
body: @beeankha, you are right!! The file transfer generates 2 commands, one for the controller node and another for the managed node. The controller node uses the `AWS S3` API to download/put file to S3 bucket while the managed node uses the `curl` command to download/upload file to bucket.

created_at: 2025-04-08T08:57:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from typing import Any
+from typing import Dict
+from typing import Optional
+
+try:
+    import boto3
+    from botocore.client import Config
+except ImportError:
+    pass
+
+
+class AwsConnectionPluginBase:
+    def __init__(self) -> None:
+        pass
+
+    def _get_boto_client(
+        self,
+        service: str,
+        region_name: Optional[str] = None,
+        endpoint_url: Optional[str] = None,
+        config: Optional[Dict[str, Any]] = None,
+    ):
path: plugins/plugin_utils/base.py
user: login: abikouo
body: The botocore does use type hint, it is hard to deduced so putting `Any` as returned value is enough
created_at: 2025-04-08T09:23:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,20 +116,74 @@ def get_url(
             client_method, Params=params, ExpiresIn=3600, HttpMethod=http_method
         )
 
-    def generate_encryption_settings(self) -> Tuple[Dict, Dict]:
-        """Generate Encryption Settings"""
-        put_args = {}
-        put_headers = {}
-        if not self.connection.get_option("bucket_sse_mode"):
-            return put_args, put_headers
-
-        put_args["ServerSideEncryption"] = self.connection.get_option("bucket_sse_mode")
-        put_headers["x-amz-server-side-encryption"] = self.connection.get_option("bucket_sse_mode")
-        if self.connection.get_option("bucket_sse_mode") == "aws:kms" and self.connection.get_option(
-            "bucket_sse_kms_key_id"
-        ):
-            put_args["SSEKMSKeyId"] = self.connection.get_option("bucket_sse_kms_key_id")
-            put_headers["x-amz-server-side-encryption-aws-kms-key-id"] = self.connection.get_option(
-                "bucket_sse_kms_key_id"
-            )
-        return put_args, put_headers
+    def generate_host_commands(
+        self,
+        bucket_name: str,
+        bucket_sse_mode: Optional[str],
+        bucket_sse_kms_key_id: Optional[str],
+        s3_path: str,
+        in_path: str,
+        out_path: str,
+        is_windows: bool,
+        method: str,
+    ) -> Tuple[str, Optional[Dict[str, str]]]:
+        """
+        Generate commands for the specified bucket, S3 path, input path, and output path.
+
+        :param bucket_name: The name of the S3 bucket used for file transfers.
+        :param s3_path: The S3 path to the file to be sent.
+        :param in_path: Input path
+        :param out_path: Output path
+        :param method: The request method to use for the command (can be "get" or "put").
+
+        :returns: A tuple containing a shell command string and optional extra arguments for the PUT request.
+        """
+
+        command = None
+        url = None
+        put_args = None
+        if method == "get":
path: plugins/plugin_utils/s3clientmanager.py
user: login: abikouo
body: The function description has been updated
created_at: 2025-04-08T09:41:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: softwarefactory-project-zuul[bot]
created_at: 2025-04-01T16:51:38Z
body: Build succeeded.
https://ansible.softwarefactory-project.io/zuul/buildset/6e9bca9c39c24ff2a3652ae6a1569174

:heavy_check_mark: [ansible-galaxy-importer ](https://ansible.softwarefactory-project.io/zuul/build/480f1776f9ac4fb5928a869d49a87d92) SUCCESS in 3m 11s (non-voting)
:heavy_check_mark: [build-ansible-collection ](https://ansible.softwarefactory-project.io/zuul/build/2b7f570e1dad4a0ead9bf065cb870e29) SUCCESS in 10m 43s
:heavy_check_mark: [ansible-test-splitter ](https://ansible.softwarefactory-project.io/zuul/build/e27f895f7da747878c9f957aed539569) SUCCESS in 4m 00s
:heavy_check_mark: [integration-community.aws-1 ](https://ansible.softwarefactory-project.io/zuul/build/96cc6aaad9154ee992ee10836dd0f0a0) SUCCESS in 23m 53s
:heavy_check_mark: [integration-community.aws-2 ](https://ansible.softwarefactory-project.io/zuul/build/604889b9489c40c7837ff224a4ec8700) SUCCESS in 16m 18s
:heavy_check_mark: [integration-community.aws-3 ](https://ansible.softwarefactory-project.io/zuul/build/15008d21320046fb979b0a8f54269014) SUCCESS in 15m 05s
:heavy_check_mark: [integration-community.aws-4 ](https://ansible.softwarefactory-project.io/zuul/build/d6fe4b31b14d4bf099dd106063f7306c) SUCCESS in 15m 50s
:heavy_check_mark: [integration-community.aws-5 ](https://ansible.softwarefactory-project.io/zuul/build/87d40ca57ef04d5787955e13fc5157d7) SUCCESS in 14m 03s
:heavy_check_mark: [integration-community.aws-6 ](https://ansible.softwarefactory-project.io/zuul/build/d16323eafe504b258aa4dfbcb321df8c) SUCCESS in 14m 35s
:heavy_check_mark: [integration-community.aws-7 ](https://ansible.softwarefactory-project.io/zuul/build/347da23971224d948d3e434cf3ca5a7b) SUCCESS in 14m 49s
:heavy_check_mark: [integration-community.aws-8 ](https://ansible.softwarefactory-project.io/zuul/build/2c3827632f9f4485b1fb4c682c9efb69) SUCCESS in 15m 46s
:heavy_check_mark: [integration-community.aws-9 ](https://ansible.softwarefactory-project.io/zuul/build/e85ff78ee67c4a008108c2c1c0ce3535) SUCCESS in 15m 00s
:heavy_check_mark: [integration-community.aws-10 ](https://ansible.softwarefactory-project.io/zuul/build/d04c0427991d496ca612048430ab6dbd) SUCCESS in 5m 29s
:heavy_check_mark: [integration-community.aws-11 ](https://ansible.softwarefactory-project.io/zuul/build/1b3cdef87a3e443d8ed9c87973ce86f0) SUCCESS in 15m 41s
Skipped 11 jobs

author_association: CONTRIBUTOR
user: login: softwarefactory-project-zuul[bot]
created_at: 2025-04-02T14:01:15Z
body: Build succeeded.
https://ansible.softwarefactory-project.io/zuul/buildset/56e266b85628437f9b3aeb6412df8fc9

:heavy_check_mark: [ansible-galaxy-importer ](https://ansible.softwarefactory-project.io/zuul/build/5c9ed17640c543e0bb83a948cef3afc4) SUCCESS in 3m 26s (non-voting)
:heavy_check_mark: [build-ansible-collection ](https://ansible.softwarefactory-project.io/zuul/build/9e8c5de2504a41f8b7e695c4d3c0ac3b) SUCCESS in 10m 35s
:heavy_check_mark: [ansible-test-splitter ](https://ansible.softwarefactory-project.io/zuul/build/78c8885c829a47ed940a58a2e03d2352) SUCCESS in 4m 00s
:heavy_check_mark: [integration-community.aws-1 ](https://ansible.softwarefactory-project.io/zuul/build/01ad4eef991d436d95241821e11723b6) SUCCESS in 23m 18s
:heavy_check_mark: [integration-community.aws-2 ](https://ansible.softwarefactory-project.io/zuul/build/479f570ff4e745a09cee023cbe75a346) SUCCESS in 13m 58s
:heavy_check_mark: [integration-community.aws-3 ](https://ansible.softwarefactory-project.io/zuul/build/dbf465e78fe64b4bbe9f02e023268f34) SUCCESS in 12m 31s
:heavy_check_mark: [integration-community.aws-4 ](https://ansible.softwarefactory-project.io/zuul/build/a2b7b00a0f004032b7c807cdf994e43f) SUCCESS in 14m 44s
:heavy_check_mark: [integration-community.aws-5 ](https://ansible.softwarefactory-project.io/zuul/build/8e1897e8032b4c319295b6fd3b95d725) SUCCESS in 24m 12s
:heavy_check_mark: [integration-community.aws-6 ](https://ansible.softwarefactory-project.io/zuul/build/e2d6925b46704bcb90373c20373fc23d) SUCCESS in 15m 17s
:heavy_check_mark: [integration-community.aws-7 ](https://ansible.softwarefactory-project.io/zuul/build/76e934e397e14e778221291beef4b0e6) SUCCESS in 13m 36s
:heavy_check_mark: [integration-community.aws-8 ](https://ansible.softwarefactory-project.io/zuul/build/b68e17e3c94b40cb92050b87b9ce12af) SUCCESS in 13m 39s
:heavy_check_mark: [integration-community.aws-9 ](https://ansible.softwarefactory-project.io/zuul/build/94ea5074b9c94cdaad97d1e9a2dacb5b) SUCCESS in 17m 22s
:heavy_check_mark: [integration-community.aws-10 ](https://ansible.softwarefactory-project.io/zuul/build/3f4b7fb93b424d2d8610af1558066561) SUCCESS in 5m 36s
:heavy_check_mark: [integration-community.aws-11 ](https://ansible.softwarefactory-project.io/zuul/build/13c7ffa63d9345b9a00e3c0dcc1c80fb) SUCCESS in 12m 39s
Skipped 11 jobs

author_association: CONTRIBUTOR
user: login: softwarefactory-project-zuul[bot]
created_at: 2025-04-07T13:01:53Z
body: Build succeeded.
https://ansible.softwarefactory-project.io/zuul/buildset/f6deb9ae1fda4e69b98a841a0bc7d091

:x: [ansible-galaxy-importer ](https://ansible.softwarefactory-project.io/zuul/build/3d98602efad24c67ac5d7f70c7884ca9) **FAILURE** in 5m 57s (non-voting)
:heavy_check_mark: [build-ansible-collection ](https://ansible.softwarefactory-project.io/zuul/build/c8a5044934b647e38b80c2fcb940f8c2) SUCCESS in 10m 39s
:heavy_check_mark: [ansible-test-splitter ](https://ansible.softwarefactory-project.io/zuul/build/6a3ae5ef4fd24ab7b0f45dd86508b971) SUCCESS in 4m 03s
:heavy_check_mark: [integration-community.aws-1 ](https://ansible.softwarefactory-project.io/zuul/build/0e2fab0bec7c49f192be52b1c3c55350) SUCCESS in 23m 05s
:heavy_check_mark: [integration-community.aws-2 ](https://ansible.softwarefactory-project.io/zuul/build/3f881e14bd564779b7e9f1328ea1b4cd) SUCCESS in 16m 06s
:heavy_check_mark: [integration-community.aws-3 ](https://ansible.softwarefactory-project.io/zuul/build/2a2fcce8eea04893ab231ae5d51fbbf5) SUCCESS in 16m 40s
:heavy_check_mark: [integration-community.aws-4 ](https://ansible.softwarefactory-project.io/zuul/build/dc59f3d7a49643e4bae710b170c5af0f) SUCCESS in 16m 16s
:heavy_check_mark: [integration-community.aws-5 ](https://ansible.softwarefactory-project.io/zuul/build/24613913984d44abaa733176664b7a40) SUCCESS in 15m 34s
:heavy_check_mark: [integration-community.aws-6 ](https://ansible.softwarefactory-project.io/zuul/build/242ac9581b7944a8b1a925bb78c0988c) SUCCESS in 13m 58s
:heavy_check_mark: [integration-community.aws-7 ](https://ansible.softwarefactory-project.io/zuul/build/58f320977c504e8bad500279a65176c1) SUCCESS in 15m 28s
:heavy_check_mark: [integration-community.aws-8 ](https://ansible.softwarefactory-project.io/zuul/build/20514b6a93bb4bc28906650f30e5cd21) SUCCESS in 16m 44s
:heavy_check_mark: [integration-community.aws-9 ](https://ansible.softwarefactory-project.io/zuul/build/c9fa4cf71b814968bf298abe3b88d3a4) SUCCESS in 17m 25s
:heavy_check_mark: [integration-community.aws-10 ](https://ansible.softwarefactory-project.io/zuul/build/57de2eab5a1849f1bb1539266d662cfd) SUCCESS in 8m 42s
:heavy_check_mark: [integration-community.aws-11 ](https://ansible.softwarefactory-project.io/zuul/build/b8f60bcb911e4b2498df357f0dcfda1b) SUCCESS in 16m 35s
Skipped 11 jobs

author_association: CONTRIBUTOR
user: login: softwarefactory-project-zuul[bot]
created_at: 2025-04-08T10:15:33Z
body: Build succeeded.
https://ansible.softwarefactory-project.io/zuul/buildset/385c175bd6fc41feb7c4cb1c3ecb117a

:x: [ansible-galaxy-importer ](https://ansible.softwarefactory-project.io/zuul/build/fbebaa1c873b4de386f65fd27e519acc) **FAILURE** in 4m 59s (non-voting)
:heavy_check_mark: [build-ansible-collection ](https://ansible.softwarefactory-project.io/zuul/build/e6a041d5defa46089426411747f2a7ec) SUCCESS in 10m 50s
:heavy_check_mark: [ansible-test-splitter ](https://ansible.softwarefactory-project.io/zuul/build/bd17545b824c47339dd9698d0240744d) SUCCESS in 4m 03s
:heavy_check_mark: [integration-community.aws-1 ](https://ansible.softwarefactory-project.io/zuul/build/d444303dcf4f43a3ab173b873fbebb4b) SUCCESS in 25m 06s
:heavy_check_mark: [integration-community.aws-2 ](https://ansible.softwarefactory-project.io/zuul/build/802fbf94d4ed4d6a9f9ed37b62c46a9d) SUCCESS in 16m 18s
:heavy_check_mark: [integration-community.aws-3 ](https://ansible.softwarefactory-project.io/zuul/build/f1d6a54c3265413cb365cd9d3b41327f) SUCCESS in 15m 03s
:heavy_check_mark: [integration-community.aws-4 ](https://ansible.softwarefactory-project.io/zuul/build/cde838e836af4d5893270d76849ae4f3) SUCCESS in 15m 19s
:heavy_check_mark: [integration-community.aws-5 ](https://ansible.softwarefactory-project.io/zuul/build/2beb5edbeab74283bd46450f0c247594) SUCCESS in 15m 35s
:heavy_check_mark: [integration-community.aws-6 ](https://ansible.softwarefactory-project.io/zuul/build/2922e43edc9540ce82247ada72f8d563) SUCCESS in 14m 14s
:heavy_check_mark: [integration-community.aws-7 ](https://ansible.softwarefactory-project.io/zuul/build/3f3f88feed1a407593115f33f53a10d0) SUCCESS in 16m 13s
:heavy_check_mark: [integration-community.aws-8 ](https://ansible.softwarefactory-project.io/zuul/build/135e826ff4734ce895824d7c2803a488) SUCCESS in 14m 05s
:heavy_check_mark: [integration-community.aws-9 ](https://ansible.softwarefactory-project.io/zuul/build/c12621cd4c1348c08c9fa40fffeff106) SUCCESS in 15m 02s
:heavy_check_mark: [integration-community.aws-10 ](https://ansible.softwarefactory-project.io/zuul/build/a18cc64de6a74e49af98e3495ac9e36e) SUCCESS in 6m 34s
:heavy_check_mark: [integration-community.aws-11 ](https://ansible.softwarefactory-project.io/zuul/build/7c85942c0a1944b9b18786ca61f59aca) SUCCESS in 16m 17s
Skipped 11 jobs

author_association: CONTRIBUTOR
user: login: softwarefactory-project-zuul[bot]
created_at: 2025-04-11T06:55:39Z
body: Build succeeded (gate pipeline).
https://ansible.softwarefactory-project.io/zuul/buildset/1f257367cdbb417aaf74b9c436bf920d

:x: [ansible-galaxy-importer ](https://ansible.softwarefactory-project.io/zuul/build/fdb352221fe449269c09dbc1c2ca17ba) **FAILURE** in 3m 47s (non-voting)
:heavy_check_mark: [build-ansible-collection ](https://ansible.softwarefactory-project.io/zuul/build/f01c0ee3d7334b4aa30b9e051cd8eef0) SUCCESS in 11m 07s
:heavy_check_mark: [ansible-test-splitter ](https://ansible.softwarefactory-project.io/zuul/build/3fe097e8f03649fb80c27b8b5a3dcdd1) SUCCESS in 3m 59s
:heavy_check_mark: [integration-community.aws-1 ](https://ansible.softwarefactory-project.io/zuul/build/facf4423fa4747c0a2f6628e08c0170a) SUCCESS in 21m 18s
:heavy_check_mark: [integration-community.aws-2 ](https://ansible.softwarefactory-project.io/zuul/build/da3eb962cbf948ae84e6b0ce9627c43c) SUCCESS in 14m 12s
:heavy_check_mark: [integration-community.aws-3 ](https://ansible.softwarefactory-project.io/zuul/build/b7de972c4970477dad2394143aa95ecc) SUCCESS in 14m 16s
:heavy_check_mark: [integration-community.aws-4 ](https://ansible.softwarefactory-project.io/zuul/build/b451c65b0f9244509be28dfcb9bbaddb) SUCCESS in 13m 01s
:heavy_check_mark: [integration-community.aws-5 ](https://ansible.softwarefactory-project.io/zuul/build/a984d9e0c5fb4b60a255d931be67dcad) SUCCESS in 13m 07s
:heavy_check_mark: [integration-community.aws-6 ](https://ansible.softwarefactory-project.io/zuul/build/5fc8e89f8463469a890bfc8b3ad3c64d) SUCCESS in 13m 27s
:heavy_check_mark: [integration-community.aws-7 ](https://ansible.softwarefactory-project.io/zuul/build/bf99d43ec554413196b325586b9c4bb0) SUCCESS in 13m 15s
:heavy_check_mark: [integration-community.aws-8 ](https://ansible.softwarefactory-project.io/zuul/build/f5354382826346e3afbd4888713598e2) SUCCESS in 13m 00s
:heavy_check_mark: [integration-community.aws-9 ](https://ansible.softwarefactory-project.io/zuul/build/4065266b9eef44878cca61644c8eff4a) SUCCESS in 13m 21s
:heavy_check_mark: [integration-community.aws-10 ](https://ansible.softwarefactory-project.io/zuul/build/8543b70aa0e1440eaceeafb1ecb07952) SUCCESS in 4m 13s
:heavy_check_mark: [integration-community.aws-11 ](https://ansible.softwarefactory-project.io/zuul/build/dc5d5230753e4d52b120041efe646d3b) SUCCESS in 14m 32s
Skipped 11 jobs

author_association: CONTRIBUTOR
user: login: patchback[bot]
created_at: 2025-04-11T06:55:43Z
body: 
### Backport to stable-9: 💚 backport PR created

✅ Backport PR branch: `patchback/backports/stable-9/b41876d3fa108eaca7bbe1c1bd909d8c9f1d173e/pr-2275`

Backported as https://github.com/ansible-collections/community.aws/pull/2280



<sub><sup>
--
🤖 @patchback
I'm built with [octomachinery](https://octomachinery.dev) and
my source is open — https://github.com/sanitizers/patchback-github-app.
</sup></sub>


author_association: NONE