Skip to content

Commit b41876d

Browse files
authored
refactor connection/aws_ssm plugin (#2275)
SUMMARY Refactor S3 operation function from connection/aws_ssm plugin Refer to: https://issues.redhat.com/browse/ACA-2100 ISSUE TYPE Feature Pull Request Reviewed-by: GomathiselviS <gomathiselvi@gmail.com> Reviewed-by: Bianca Henderson <beeankha@gmail.com> Reviewed-by: Bikouo Aubin
1 parent 2c1e8c4 commit b41876d

File tree

5 files changed

+616
-715
lines changed

5 files changed

+616
-715
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
minor_changes:
2+
- aws_ssm - Refactor S3 operations methods for improved clarity (https://github.com/ansible-collections/community.aws/pull/2275).

plugins/connection/aws_ssm.py

Lines changed: 53 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -334,20 +334,12 @@
334334
import time
335335
from functools import wraps
336336
from typing import Any
337-
from typing import Dict
338337
from typing import Iterator
339338
from typing import List
340339
from typing import NoReturn
341-
from typing import Optional
342340
from typing import Tuple
343341
from typing import TypedDict
344342

345-
try:
346-
import boto3
347-
from botocore.client import Config
348-
except ImportError:
349-
pass
350-
351343
from ansible.errors import AnsibleConnectionFailure
352344
from ansible.errors import AnsibleError
353345
from ansible.errors import AnsibleFileNotFound
@@ -359,7 +351,7 @@
359351
from ansible.utils.display import Display
360352

361353
from ansible_collections.amazon.aws.plugins.module_utils.botocore import HAS_BOTO3
362-
354+
from ansible_collections.community.aws.plugins.plugin_utils.base import AwsConnectionPluginBase
363355
from ansible_collections.community.aws.plugins.plugin_utils.s3clientmanager import S3ClientManager
364356
from ansible_collections.community.aws.plugins.plugin_utils.terminalmanager import TerminalManager
365357

@@ -455,7 +447,7 @@ class CommandResult(TypedDict):
455447
stderr_combined: str
456448

457449

458-
class Connection(ConnectionBase):
450+
class Connection(ConnectionBase, AwsConnectionPluginBase):
459451
"""AWS SSM based connections"""
460452

461453
transport = "community.aws.aws_ssm"
@@ -467,7 +459,6 @@ class Connection(ConnectionBase):
467459
is_windows = False
468460

469461
_client = None
470-
_s3_client = None
471462
_session = None
472463
_stdout = None
473464
_session_id = ""
@@ -514,40 +505,36 @@ def _init_clients(self) -> None:
514505
"""
515506

516507
self.verbosity_display(4, "INITIALIZE BOTO3 CLIENTS")
517-
profile_name = self.get_option("profile") or ""
518-
region_name = self.get_option("region")
519-
520-
# Initialize S3ClientManager
521-
self.s3_manager = S3ClientManager(self)
522508

523-
# Initialize S3 client
524-
s3_endpoint_url, s3_region_name = self.s3_manager.get_bucket_endpoint()
525-
self.verbosity_display(4, f"SETUP BOTO3 CLIENTS: S3 {s3_endpoint_url}")
526-
self.s3_manager.initialize_client(
527-
region_name=s3_region_name, endpoint_url=s3_endpoint_url, profile_name=profile_name
509+
# Create S3 and SSM clients
510+
config = {"signature_version": "s3v4", "s3": {"addressing_style": self.get_option("s3_addressing_style")}}
511+
512+
bucket_endpoint_url = self.get_option("bucket_endpoint_url")
513+
s3_endpoint_url, s3_region_name = S3ClientManager.get_bucket_endpoint(
514+
bucket_name=self.get_option("bucket_name"),
515+
bucket_endpoint_url=bucket_endpoint_url,
516+
access_key_id=self.get_option("access_key_id"),
517+
secret_key_id=self.get_option("secret_access_key"),
518+
session_token=self.get_option("session_token"),
519+
region_name=self.get_option("region"),
520+
profile_name=self.get_option("profile"),
528521
)
529-
self._s3_client = self.s3_manager._s3_client
530522

531-
# Initialize SSM client
532-
self._initialize_ssm_client(region_name, profile_name)
523+
self.verbosity_display(4, f"BUCKET Information - Endpoint: {s3_endpoint_url} / Region: {s3_region_name}")
533524

534-
def _initialize_ssm_client(self, region_name: Optional[str], profile_name: str) -> None:
535-
"""
536-
Initializes the SSM client used to manage sessions.
537-
Args:
538-
region_name (Optional[str]): AWS region for the SSM client.
539-
profile_name (str): AWS profile name for authentication.
525+
# Initialize S3ClientManager
526+
s3_client = self._get_boto_client("s3", endpoint_url=s3_endpoint_url, region_name=s3_region_name, config=config)
527+
self.s3_manager = S3ClientManager(s3_client)
540528

541-
Returns:
542-
None
543-
"""
529+
# Initialize SSM client
530+
self._client = self._get_boto_client("ssm", region_name=self.get_option("region"), config=config)
544531

545-
self.verbosity_display(4, "SETUP BOTO3 CLIENTS: SSM")
546-
self._client = self._get_boto_client(
547-
"ssm",
548-
region_name=region_name,
549-
profile_name=profile_name,
550-
)
532+
@property
533+
def s3_client(self) -> None:
534+
client = None
535+
if self.s3_manager is not None:
536+
client = self.s3_manager.client
537+
return client
551538

552539
def verbosity_display(self, level: int, message: str) -> None:
553540
"""
@@ -616,6 +603,7 @@ def start_session(self):
616603
if document_name is not None:
617604
start_session_args["DocumentName"] = document_name
618605
response = self._client.start_session(**start_session_args)
606+
self.verbosity_display(4, f"START SESSION RESPONSE: {response}")
619607
self._session_id = response["SessionId"]
620608

621609
region_name = self.get_option("region")
@@ -803,153 +791,26 @@ def _flush_stderr(self, session_process) -> str:
803791

804792
return stderr
805793

806-
def _get_boto_client(self, service, region_name=None, profile_name=None, endpoint_url=None):
807-
"""Gets a boto3 client based on the STS token"""
808-
809-
aws_access_key_id = self.get_option("access_key_id")
810-
aws_secret_access_key = self.get_option("secret_access_key")
811-
aws_session_token = self.get_option("session_token")
812-
813-
session_args = dict(
814-
aws_access_key_id=aws_access_key_id,
815-
aws_secret_access_key=aws_secret_access_key,
816-
aws_session_token=aws_session_token,
817-
region_name=region_name,
818-
)
819-
if profile_name:
820-
session_args["profile_name"] = profile_name
821-
session = boto3.session.Session(**session_args)
822-
823-
client = session.client(
824-
service,
825-
endpoint_url=endpoint_url,
826-
config=Config(
827-
signature_version="s3v4",
828-
s3={"addressing_style": self.get_option("s3_addressing_style")},
829-
),
830-
)
831-
return client
832-
833794
def _escape_path(self, path: str) -> str:
834795
return path.replace("\\", "/")
835796

836-
def _generate_commands(
837-
self,
838-
bucket_name: str,
839-
s3_path: str,
840-
in_path: str,
841-
out_path: str,
842-
) -> Tuple[List[Dict], dict]:
843-
"""
844-
Generate commands for the specified bucket, S3 path, input path, and output path.
845-
846-
:param bucket_name: The name of the S3 bucket used for file transfers.
847-
:param s3_path: The S3 path to the file to be sent.
848-
:param in_path: Input path
849-
:param out_path: Output path
850-
:param method: The request method to use for the command (can be "get" or "put").
851-
852-
:returns: A tuple containing a list of command dictionaries along with any ``put_args`` dictionaries.
853-
"""
854-
855-
put_args, put_headers = self.s3_manager.generate_encryption_settings()
856-
commands = []
857-
858-
put_url = self.s3_manager.get_url("put_object", bucket_name, s3_path, "PUT", extra_args=put_args)
859-
get_url = self.s3_manager.get_url("get_object", bucket_name, s3_path, "GET")
860-
861-
if self.is_windows:
862-
put_command_headers = "; ".join([f"'{h}' = '{v}'" for h, v in put_headers.items()])
863-
commands.append({
864-
"command":
865-
(
866-
"Invoke-WebRequest "
867-
f"'{get_url}' "
868-
f"-OutFile '{out_path}'"
869-
),
870-
# The "method" key indicates to _file_transport_command which commands are get_commands
871-
"method": "get",
872-
"headers": {},
873-
}) # fmt: skip
874-
commands.append({
875-
"command":
876-
(
877-
"Invoke-WebRequest -Method PUT "
878-
# @{'key' = 'value'; 'key2' = 'value2'}
879-
f"-Headers @{{{put_command_headers}}} "
880-
f"-InFile '{in_path}' "
881-
f"-Uri '{put_url}' "
882-
f"-UseBasicParsing"
883-
),
884-
# The "method" key indicates to _file_transport_command which commands are put_commands
885-
"method": "put",
886-
"headers": put_headers,
887-
}) # fmt: skip
888-
else:
889-
put_command_headers = " ".join([f"-H '{h}: {v}'" for h, v in put_headers.items()])
890-
commands.append({
891-
"command":
892-
(
893-
"curl "
894-
f"-o '{out_path}' "
895-
f"'{get_url}'"
896-
),
897-
# The "method" key indicates to _file_transport_command which commands are get_commands
898-
"method": "get",
899-
"headers": {},
900-
}) # fmt: skip
901-
# Due to https://github.com/curl/curl/issues/183 earlier
902-
# versions of curl did not create the output file, when the
903-
# response was empty. Although this issue was fixed in 2015,
904-
# some actively maintained operating systems still use older
905-
# versions of it (e.g. CentOS 7)
906-
commands.append({
907-
"command":
908-
(
909-
"touch "
910-
f"'{out_path}'"
911-
),
912-
"method": "get",
913-
"headers": {},
914-
}) # fmt: skip
915-
commands.append({
916-
"command":
917-
(
918-
"curl --request PUT "
919-
f"{put_command_headers} "
920-
f"--upload-file '{in_path}' "
921-
f"'{put_url}'"
922-
),
923-
# The "method" key indicates to _file_transport_command which commands are put_commands
924-
"method": "put",
925-
"headers": put_headers,
926-
}) # fmt: skip
927-
928-
return commands, put_args
929-
930-
def _exec_transport_commands(self, in_path: str, out_path: str, commands: List[dict]) -> CommandResult:
797+
def _exec_transport_commands(self, in_path: str, out_path: str, command: dict) -> CommandResult:
931798
"""
932-
Execute the provided transport commands.
799+
Execute the provided transport command.
933800
934801
:param in_path: The input path.
935802
:param out_path: The output path.
936-
:param commands: A list of command dictionaries containing the command string and metadata.
803+
:param command: A command to execute on the host.
937804
938805
:returns: A tuple containing the return code, stdout, and stderr.
939806
"""
940807

941-
stdout_combined, stderr_combined = "", ""
942-
for command in commands:
943-
(returncode, stdout, stderr) = self.exec_command(command["command"], in_data=None, sudoable=False)
944-
945-
# Check the return code
946-
if returncode != 0:
947-
raise AnsibleError(f"failed to transfer file to {in_path} {out_path}:\n{stdout}\n{stderr}")
808+
returncode, stdout, stderr = self.exec_command(command, in_data=None, sudoable=False)
809+
# Check the return code
810+
if returncode != 0:
811+
raise AnsibleError(f"failed to transfer file to {in_path} {out_path}:\n{stdout}\n{stderr}")
948812

949-
stdout_combined += stdout
950-
stderr_combined += stderr
951-
952-
return (returncode, stdout_combined, stderr_combined)
813+
return returncode, stdout, stderr
953814

954815
@_ssm_retry
955816
def _file_transport_command(
@@ -971,30 +832,30 @@ def _file_transport_command(
971832
bucket_name = self.get_option("bucket_name")
972833
s3_path = self._escape_path(f"{self.instance_id}/{out_path}")
973834

974-
client = self._s3_client
975-
976-
commands, put_args = self._generate_commands(
835+
command, put_args = self.s3_manager.generate_host_commands(
977836
bucket_name,
837+
self.get_option("bucket_sse_mode"),
838+
self.get_option("bucket_sse_kms_key_id"),
978839
s3_path,
979840
in_path,
980841
out_path,
842+
self.is_windows,
843+
ssm_action,
981844
)
982845

983846
try:
984847
if ssm_action == "get":
985-
put_commands = [cmd for cmd in commands if cmd.get("method") == "put"]
986-
result = self._exec_transport_commands(in_path, out_path, put_commands)
848+
result = self._exec_transport_commands(in_path, out_path, command)
987849
with open(to_bytes(out_path, errors="surrogate_or_strict"), "wb") as data:
988-
client.download_fileobj(bucket_name, s3_path, data)
850+
self.s3_client.download_fileobj(bucket_name, s3_path, data)
989851
else:
990-
get_commands = [cmd for cmd in commands if cmd.get("method") == "get"]
991852
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as data:
992-
client.upload_fileobj(data, bucket_name, s3_path, ExtraArgs=put_args)
993-
result = self._exec_transport_commands(in_path, out_path, get_commands)
853+
self.s3_client.upload_fileobj(data, bucket_name, s3_path, ExtraArgs=put_args)
854+
result = self._exec_transport_commands(in_path, out_path, command)
994855
return result
995856
finally:
996857
# Remove the files from the bucket after they've been transferred
997-
client.delete_object(Bucket=bucket_name, Key=s3_path)
858+
self.s3_client.delete_object(Bucket=bucket_name, Key=s3_path)
998859

999860
def put_file(self, in_path: str, out_path: str) -> Tuple[int, str, str]:
1000861
"""transfer a file from local to remote"""
@@ -1019,12 +880,14 @@ def close(self) -> None:
1019880
"""terminate the connection"""
1020881
if self._session_id:
1021882
self.verbosity_display(3, f"CLOSING SSM CONNECTION TO: {self.instance_id}")
1022-
if self._has_timeout:
1023-
self._session.terminate()
1024-
else:
1025-
cmd = b"\nexit\n"
1026-
self._session.communicate(cmd)
883+
if self._session is not None:
884+
if self._has_timeout:
885+
self._session.terminate()
886+
else:
887+
cmd = b"\nexit\n"
888+
self._session.communicate(cmd)
1027889

1028890
self.verbosity_display(4, f"TERMINATE SSM SESSION: {self._session_id}")
1029-
self._client.terminate_session(SessionId=self._session_id)
891+
if self._client:
892+
self._client.terminate_session(SessionId=self._session_id)
1030893
self._session_id = ""

plugins/plugin_utils/base.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright: Contributors to the Ansible project
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from typing import Any
7+
from typing import Dict
8+
from typing import Optional
9+
10+
try:
11+
from boto3.session import Session
12+
from botocore.client import Config
13+
except ImportError:
14+
pass
15+
16+
17+
class AwsConnectionPluginBase:
18+
def __init__(self) -> None:
19+
pass
20+
21+
def _get_boto_client(
22+
self,
23+
service: str,
24+
region_name: Optional[str] = None,
25+
endpoint_url: Optional[str] = None,
26+
config: Optional[Dict[str, Any]] = None,
27+
) -> Any:
28+
"""Gets a boto3 client based on the STS token"""
29+
30+
aws_access_key_id = self.get_option("access_key_id")
31+
aws_secret_access_key = self.get_option("secret_access_key")
32+
aws_session_token = self.get_option("session_token")
33+
34+
session_args = dict(
35+
aws_access_key_id=aws_access_key_id,
36+
aws_secret_access_key=aws_secret_access_key,
37+
aws_session_token=aws_session_token,
38+
region_name=region_name,
39+
)
40+
profile_name = self.get_option("profile")
41+
if profile_name:
42+
session_args["profile_name"] = profile_name
43+
session = Session(**session_args)
44+
params = {}
45+
if endpoint_url:
46+
params["endpoint_url"] = endpoint_url
47+
if config:
48+
params["config"] = Config(**config)
49+
50+
return session.client(service, **params)

0 commit comments

Comments
 (0)