332
332
import string
333
333
import subprocess
334
334
import time
335
+ from typing import Dict
336
+ from typing import List
335
337
from typing import NoReturn
336
338
from typing import Optional
337
339
from typing import Tuple
340
+ from typing import TypedDict
338
341
339
342
try :
340
343
import boto3
@@ -439,6 +442,16 @@ def filter_ansi(line: str, is_windows: bool) -> str:
439
442
return line
440
443
441
444
445
+ class CommandResult (TypedDict ):
446
+ """
447
+ A dictionary that contains the executed command results.
448
+ """
449
+
450
+ returncode : int
451
+ stdout_combined : str
452
+ stderr_combined : str
453
+
454
+
442
455
class Connection (ConnectionBase ):
443
456
"""AWS SSM based connections"""
444
457
@@ -974,15 +987,46 @@ def _generate_encryption_settings(self):
974
987
put_headers ["x-amz-server-side-encryption-aws-kms-key-id" ] = self .get_option ("bucket_sse_kms_key_id" )
975
988
return put_args , put_headers
976
989
977
- def _generate_commands (self , bucket_name , s3_path , in_path , out_path ):
990
+ def _generate_commands (
991
+ self ,
992
+ bucket_name : str ,
993
+ s3_path : str ,
994
+ in_path : str ,
995
+ out_path : str ,
996
+ ) -> Tuple [List [Dict ], dict ]:
997
+ """
998
+ Generate commands for the specified bucket, S3 path, input path, and output path.
999
+
1000
+ :param bucket_name: The name of the S3 bucket used for file transfers.
1001
+ :param s3_path: The S3 path to the file to be sent.
1002
+ :param in_path: Input path
1003
+ :param out_path: Output path
1004
+ :param method: The request method to use for the command (can be "get" or "put").
1005
+
1006
+ :returns: A tuple containing a list of command dictionaries along with any ``put_args`` dictionaries.
1007
+ """
1008
+
978
1009
put_args , put_headers = self ._generate_encryption_settings ()
1010
+ commands = []
979
1011
980
1012
put_url = self ._get_url ("put_object" , bucket_name , s3_path , "PUT" , extra_args = put_args )
981
1013
get_url = self ._get_url ("get_object" , bucket_name , s3_path , "GET" )
982
1014
983
1015
if self .is_windows :
984
1016
put_command_headers = "; " .join ([f"'{ h } ' = '{ v } '" for h , v in put_headers .items ()])
985
- put_commands = [
1017
+ commands .append ({
1018
+ "command" :
1019
+ (
1020
+ "Invoke-WebRequest "
1021
+ f"'{ get_url } ' "
1022
+ f"-OutFile '{ out_path } '"
1023
+ ),
1024
+ # The "method" key indicates to _file_transport_command which commands are get_commands
1025
+ "method" : "get" ,
1026
+ "headers" : {},
1027
+ }) # fmt: skip
1028
+ commands .append ({
1029
+ "command" :
986
1030
(
987
1031
"Invoke-WebRequest -Method PUT "
988
1032
# @{'key' = 'value'; 'key2' = 'value2'}
@@ -991,47 +1035,66 @@ def _generate_commands(self, bucket_name, s3_path, in_path, out_path):
991
1035
f"-Uri '{ put_url } ' "
992
1036
f"-UseBasicParsing"
993
1037
),
994
- ] # fmt: skip
995
- get_commands = [
996
- (
997
- "Invoke-WebRequest "
998
- f"'{ get_url } ' "
999
- f"-OutFile '{ out_path } '"
1000
- ),
1001
- ] # fmt: skip
1038
+ # The "method" key indicates to _file_transport_command which commands are put_commands
1039
+ "method" : "put" ,
1040
+ "headers" : put_headers ,
1041
+ }) # fmt: skip
1002
1042
else :
1003
1043
put_command_headers = " " .join ([f"-H '{ h } : { v } '" for h , v in put_headers .items ()])
1004
- put_commands = [
1005
- (
1006
- "curl --request PUT "
1007
- f"{ put_command_headers } "
1008
- f"--upload-file '{ in_path } ' "
1009
- f"'{ put_url } '"
1010
- ),
1011
- ] # fmt: skip
1012
- get_commands = [
1044
+ commands .append ({
1045
+ "command" :
1013
1046
(
1014
1047
"curl "
1015
1048
f"-o '{ out_path } ' "
1016
1049
f"'{ get_url } '"
1017
1050
),
1018
- # Due to https://github.com/curl/curl/issues/183 earlier
1019
- # versions of curl did not create the output file, when the
1020
- # response was empty. Although this issue was fixed in 2015,
1021
- # some actively maintained operating systems still use older
1022
- # versions of it (e.g. CentOS 7)
1051
+ # The "method" key indicates to _file_transport_command which commands are get_commands
1052
+ "method" : "get" ,
1053
+ "headers" : {},
1054
+ }) # fmt: skip
1055
+ # Due to https://github.com/curl/curl/issues/183 earlier
1056
+ # versions of curl did not create the output file, when the
1057
+ # response was empty. Although this issue was fixed in 2015,
1058
+ # some actively maintained operating systems still use older
1059
+ # versions of it (e.g. CentOS 7)
1060
+ commands .append ({
1061
+ "command" :
1023
1062
(
1024
1063
"touch "
1025
1064
f"'{ out_path } '"
1026
- )
1027
- ] # fmt: skip
1065
+ ),
1066
+ "method" : "get" ,
1067
+ "headers" : {},
1068
+ }) # fmt: skip
1069
+ commands .append ({
1070
+ "command" :
1071
+ (
1072
+ "curl --request PUT "
1073
+ f"{ put_command_headers } "
1074
+ f"--upload-file '{ in_path } ' "
1075
+ f"'{ put_url } '"
1076
+ ),
1077
+ # The "method" key indicates to _file_transport_command which commands are put_commands
1078
+ "method" : "put" ,
1079
+ "headers" : put_headers ,
1080
+ }) # fmt: skip
1081
+
1082
+ return commands , put_args
1028
1083
1029
- return get_commands , put_commands , put_args
1084
+ def _exec_transport_commands (self , in_path : str , out_path : str , commands : List [dict ]) -> CommandResult :
1085
+ """
1086
+ Execute the provided transport commands.
1087
+
1088
+ :param in_path: The input path.
1089
+ :param out_path: The output path.
1090
+ :param commands: A list of command dictionaries containing the command string and metadata.
1091
+
1092
+ :returns: A tuple containing the return code, stdout, and stderr.
1093
+ """
1030
1094
1031
- def _exec_transport_commands (self , in_path , out_path , commands ):
1032
1095
stdout_combined , stderr_combined = "" , ""
1033
1096
for command in commands :
1034
- (returncode , stdout , stderr ) = self .exec_command (command , in_data = None , sudoable = False )
1097
+ (returncode , stdout , stderr ) = self .exec_command (command [ "command" ] , in_data = None , sudoable = False )
1035
1098
1036
1099
# Check the return code
1037
1100
if returncode != 0 :
@@ -1043,31 +1106,46 @@ def _exec_transport_commands(self, in_path, out_path, commands):
1043
1106
return (returncode , stdout_combined , stderr_combined )
1044
1107
1045
1108
@_ssm_retry
1046
- def _file_transport_command (self , in_path , out_path , ssm_action ):
1047
- """transfer a file to/from host using an intermediate S3 bucket"""
1109
+ def _file_transport_command (
1110
+ self ,
1111
+ in_path : str ,
1112
+ out_path : str ,
1113
+ ssm_action : str ,
1114
+ ) -> CommandResult :
1115
+ """
1116
+ Transfer file(s) to/from host using an intermediate S3 bucket and then delete the file(s).
1117
+
1118
+ :param in_path: The input path.
1119
+ :param out_path: The output path.
1120
+ :param ssm_action: The SSM action to perform ("get" or "put").
1121
+
1122
+ :returns: The command's return code, stdout, and stderr in a tuple.
1123
+ """
1048
1124
1049
1125
bucket_name = self .get_option ("bucket_name" )
1050
1126
s3_path = self ._escape_path (f"{ self .instance_id } /{ out_path } " )
1051
1127
1052
- get_commands , put_commands , put_args = self ._generate_commands (
1128
+ client = self ._s3_client
1129
+
1130
+ commands , put_args = self ._generate_commands (
1053
1131
bucket_name ,
1054
1132
s3_path ,
1055
1133
in_path ,
1056
1134
out_path ,
1057
1135
)
1058
1136
1059
- client = self ._s3_client
1060
-
1061
1137
try :
1062
1138
if ssm_action == "get" :
1063
- (returncode , stdout , stderr ) = self ._exec_transport_commands (in_path , out_path , put_commands )
1139
+ put_commands = [cmd for cmd in commands if cmd .get ("method" ) == "put" ]
1140
+ result = self ._exec_transport_commands (in_path , out_path , put_commands )
1064
1141
with open (to_bytes (out_path , errors = "surrogate_or_strict" ), "wb" ) as data :
1065
1142
client .download_fileobj (bucket_name , s3_path , data )
1066
1143
else :
1144
+ get_commands = [cmd for cmd in commands if cmd .get ("method" ) == "get" ]
1067
1145
with open (to_bytes (in_path , errors = "surrogate_or_strict" ), "rb" ) as data :
1068
1146
client .upload_fileobj (data , bucket_name , s3_path , ExtraArgs = put_args )
1069
- ( returncode , stdout , stderr ) = self ._exec_transport_commands (in_path , out_path , get_commands )
1070
- return ( returncode , stdout , stderr )
1147
+ result = self ._exec_transport_commands (in_path , out_path , get_commands )
1148
+ return result
1071
1149
finally :
1072
1150
# Remove the files from the bucket after they've been transferred
1073
1151
client .delete_object (Bucket = bucket_name , Key = s3_path )
0 commit comments