334
334
import time
335
335
from functools import wraps
336
336
from typing import Any
337
- from typing import Dict
338
337
from typing import Iterator
339
338
from typing import List
340
339
from typing import NoReturn
341
- from typing import Optional
342
340
from typing import Tuple
343
341
from typing import TypedDict
344
342
345
- try :
346
- import boto3
347
- from botocore .client import Config
348
- except ImportError :
349
- pass
350
-
351
343
from ansible .errors import AnsibleConnectionFailure
352
344
from ansible .errors import AnsibleError
353
345
from ansible .errors import AnsibleFileNotFound
359
351
from ansible .utils .display import Display
360
352
361
353
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
363
355
from ansible_collections .community .aws .plugins .plugin_utils .s3clientmanager import S3ClientManager
364
356
from ansible_collections .community .aws .plugins .plugin_utils .terminalmanager import TerminalManager
365
357
@@ -455,7 +447,7 @@ class CommandResult(TypedDict):
455
447
stderr_combined : str
456
448
457
449
458
- class Connection (ConnectionBase ):
450
+ class Connection (ConnectionBase , AwsConnectionPluginBase ):
459
451
"""AWS SSM based connections"""
460
452
461
453
transport = "community.aws.aws_ssm"
@@ -467,7 +459,6 @@ class Connection(ConnectionBase):
467
459
is_windows = False
468
460
469
461
_client = None
470
- _s3_client = None
471
462
_session = None
472
463
_stdout = None
473
464
_session_id = ""
@@ -514,40 +505,36 @@ def _init_clients(self) -> None:
514
505
"""
515
506
516
507
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 )
522
508
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" ),
528
521
)
529
- self ._s3_client = self .s3_manager ._s3_client
530
522
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 } " )
533
524
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 )
540
528
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 )
544
531
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
551
538
552
539
def verbosity_display (self , level : int , message : str ) -> None :
553
540
"""
@@ -616,6 +603,7 @@ def start_session(self):
616
603
if document_name is not None :
617
604
start_session_args ["DocumentName" ] = document_name
618
605
response = self ._client .start_session (** start_session_args )
606
+ self .verbosity_display (4 , f"START SESSION RESPONSE: { response } " )
619
607
self ._session_id = response ["SessionId" ]
620
608
621
609
region_name = self .get_option ("region" )
@@ -803,153 +791,26 @@ def _flush_stderr(self, session_process) -> str:
803
791
804
792
return stderr
805
793
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
-
833
794
def _escape_path (self , path : str ) -> str :
834
795
return path .replace ("\\ " , "/" )
835
796
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 :
931
798
"""
932
- Execute the provided transport commands .
799
+ Execute the provided transport command .
933
800
934
801
:param in_path: The input path.
935
802
: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 .
937
804
938
805
:returns: A tuple containing the return code, stdout, and stderr.
939
806
"""
940
807
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 } " )
948
812
949
- stdout_combined += stdout
950
- stderr_combined += stderr
951
-
952
- return (returncode , stdout_combined , stderr_combined )
813
+ return returncode , stdout , stderr
953
814
954
815
@_ssm_retry
955
816
def _file_transport_command (
@@ -971,30 +832,30 @@ def _file_transport_command(
971
832
bucket_name = self .get_option ("bucket_name" )
972
833
s3_path = self ._escape_path (f"{ self .instance_id } /{ out_path } " )
973
834
974
- client = self ._s3_client
975
-
976
- commands , put_args = self ._generate_commands (
835
+ command , put_args = self .s3_manager .generate_host_commands (
977
836
bucket_name ,
837
+ self .get_option ("bucket_sse_mode" ),
838
+ self .get_option ("bucket_sse_kms_key_id" ),
978
839
s3_path ,
979
840
in_path ,
980
841
out_path ,
842
+ self .is_windows ,
843
+ ssm_action ,
981
844
)
982
845
983
846
try :
984
847
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 )
987
849
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 )
989
851
else :
990
- get_commands = [cmd for cmd in commands if cmd .get ("method" ) == "get" ]
991
852
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 )
994
855
return result
995
856
finally :
996
857
# 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 )
998
859
999
860
def put_file (self , in_path : str , out_path : str ) -> Tuple [int , str , str ]:
1000
861
"""transfer a file from local to remote"""
@@ -1019,12 +880,14 @@ def close(self) -> None:
1019
880
"""terminate the connection"""
1020
881
if self ._session_id :
1021
882
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"\n exit\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"\n exit\n "
888
+ self ._session .communicate (cmd )
1027
889
1028
890
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 )
1030
893
self ._session_id = ""
0 commit comments