1
+ import logging , logging .handlers
2
+ import argparse
3
+ import os
4
+ import boto3
5
+ import hashlib
6
+ import tempfile
7
+ import shutil
8
+
9
+ logging .getLogger ("botocore" ).setLevel (logging .CRITICAL )
10
+
11
+ def get_sha256_hash (file_path ):
12
+ logging .debug ('Hashing "%s" using SHA256' % file_path )
13
+ BUF_SIZE = 65536 # lets read stuff in 64kb chunks!
14
+ sha256 = hashlib .sha256 ()
15
+ with open (file_path , 'rb' ) as f :
16
+ while True :
17
+ data = f .read (BUF_SIZE )
18
+ if not data :
19
+ break
20
+ sha256 .update (data )
21
+
22
+ logging .debug (' SHA256 hash: {0}' .format (sha256 .hexdigest ()))
23
+ return sha256 .digest ()
24
+
25
+
26
+ def process_parameters_with_prefix (param_prefix , cred_path , aws_region , aws_access_key = None , aws_secret_key = None , dryrun = False ):
27
+ logging .debug ('Searching for parameters with a prefix of %s' % param_prefix )
28
+
29
+ def get_parameters (parameter_names_list ):
30
+ parameter_list = []
31
+ if parameter_names_list :
32
+ result = ssm .get_parameters (Names = parameter_names_list , WithDecryption = True )
33
+ if result :
34
+ if 'ResponseMetadata' in result :
35
+ if 'HTTPStatusCode' in result ['ResponseMetadata' ]:
36
+ if result ['ResponseMetadata' ]['HTTPStatusCode' ] == 200 :
37
+ if 'Parameters' in result :
38
+ parameter_list = result ['Parameters' ]
39
+ return parameter_list
40
+
41
+ def process_parameter (param_name , param_value ):
42
+ filename = param_name .split (param_prefix )[1 ]
43
+ full_cred_path = cred_path + os .sep + filename
44
+ existing_file_sha256_hash = None
45
+ if os .path .exists (full_cred_path ):
46
+ existing_file_sha256_hash = get_sha256_hash (full_cred_path )
47
+ new_file_full_path = temp_dir + os .sep + filename + '.new'
48
+ logging .debug ('Storing retrieved value for parameter "%s" in "%s"' % (param_name , new_file_full_path ))
49
+ with open (new_file_full_path , 'w' ) as f :
50
+ f .write (param_value .replace ('\\ n' , '\n ' ))
51
+ new_file_sha256_hash = get_sha256_hash (new_file_full_path )
52
+ logging .debug ('Comparing file hashes' )
53
+ if existing_file_sha256_hash != new_file_sha256_hash :
54
+ if not existing_file_sha256_hash :
55
+ logging .info ('This is a new credentials file: "%s"' % filename )
56
+ else :
57
+ logging .info ("Contents don't match - replacing existing file contents with value from parameter store" )
58
+ if not dryrun :
59
+ if os .path .exists (new_file_full_path ) and os .stat (new_file_full_path ).st_size > 0 :
60
+ shutil .copyfile (new_file_full_path , full_cred_path )
61
+ else :
62
+ logging .error ('file %s is missing or zero length - NOT replacing' % new_file_full_path )
63
+ else :
64
+ logging .info ('*** Dryrun selected - will NOT update "%s"' % full_cred_path )
65
+ else :
66
+ logging .info ('Contents of existing "%s" MATCH with value for "%s" from parameter store' % (full_cred_path , param_name ))
67
+
68
+ # Cleanup
69
+ if new_file_full_path :
70
+ logging .debug ('Removing %s' % new_file_full_path )
71
+ os .remove (new_file_full_path )
72
+
73
+ def get_parameters_with_prefix (prefix ):
74
+ parameter_names_list = []
75
+ result = ssm .describe_parameters (Filters = [{'Key' : 'Name' , 'Values' : [param_prefix ]}], MaxResults = 50 )
76
+ if result :
77
+ if 'ResponseMetadata' in result :
78
+ if 'HTTPStatusCode' in result ['ResponseMetadata' ]:
79
+ if result ['ResponseMetadata' ]['HTTPStatusCode' ] == 200 :
80
+ if 'Parameters' in result :
81
+ for param in result ['Parameters' ]:
82
+ logging .debug ('Found parameter "%s" - adding to list' % param ['Name' ])
83
+ parameter_names_list .append (param ['Name' ])
84
+ next_token = None
85
+ if 'NextToken' in result :
86
+ next_token = result ['NextToken' ]
87
+ while next_token :
88
+ result = ssm .describe_parameters (Filters = [{'Key' : 'Name' , 'Values' : [param_prefix ]}], MaxResults = 50 )
89
+ if result :
90
+ if 'ResponseMetadata' in result :
91
+ if 'HTTPStatusCode' in result ['ResponseMetadata' ]:
92
+ if result ['ResponseMetadata' ]['HTTPStatusCode' ] == 200 :
93
+ if 'Parameters' in result :
94
+ for param in result ['Parameters' ]:
95
+ logging .debug ('Found parameter "%s" - adding to list' % param ['Name' ])
96
+ parameter_names_list .append (param ['Name' ])
97
+ next_token = None
98
+ if 'NextToken' in result :
99
+ next_token = result ['NextToken' ]
100
+ return parameter_names_list
101
+
102
+ # If aws_access_key and aws_secret_key provided, use those
103
+ if aws_access_key and aws_secret_key :
104
+ session = boto3 .session .Session (aws_access_key_id = aws_access_key ,
105
+ aws_secret_access_key = aws_secret_key ,
106
+ region_name = aws_region )
107
+ else :
108
+ session = boto3 .session .Session (region_name = aws_region )
109
+
110
+ ssm = session .client ('ssm' )
111
+
112
+ parameter_names_list = get_parameters_with_prefix (param_prefix )
113
+
114
+ if parameter_names_list :
115
+ # Make sure we have a temp dir to work with
116
+ temp_dir = tempfile .gettempdir ()
117
+ if not os .path .exists (temp_dir ):
118
+ os .makedirs (temp_dir )
119
+
120
+ for param in get_parameters (parameter_names_list ):
121
+ parameter_name = param ['Name' ]
122
+ parameter_value = param ['Value' ]
123
+ process_parameter (parameter_name , parameter_value )
124
+
125
+
126
+ if __name__ == "__main__" :
127
+
128
+ LOG_FILENAME = 'parameter-sync.log'
129
+
130
+ description = "Script to get all parameters from AWS Parameter\n "
131
+ description += "Store with a prefix that matches the given prefix\n \n "
132
+ description += "Note: The following environment variables can be set prior to execution\n "
133
+ description += " of the script (or alternatively, set them using script parameters)\n \n "
134
+ description += " AWS_ACCESS_KEY_ID\n "
135
+ description += " AWS_SECRET_ACCESS_KEY"
136
+
137
+ parser = argparse .ArgumentParser (description = description , formatter_class = argparse .RawTextHelpFormatter )
138
+
139
+ parser .add_argument ("--aws-access-key-id" , help = "AWS Access Key ID" , dest = 'aws_access_key' , required = False )
140
+ parser .add_argument ("--aws-secret-access-key" , help = "AWS Secret Access Key" , dest = 'aws_secret_key' , required = False )
141
+ parser .add_argument ("--aws-region" , help = "AWS Region" , dest = 'aws_region' , required = True )
142
+ parser .add_argument ("--param-prefix" , help = "Parameter prefix" , dest = 'param_prefix' , required = True )
143
+ parser .add_argument ("--credentials-path" , help = "Where credentials are stored" , dest = 'cred_path' , default = '/credentials/' )
144
+ parser .add_argument ("--verbose" , help = "Turn on DEBUG logging" , action = 'store_true' , required = False )
145
+ parser .add_argument ("--dryrun" , help = "Do a dryrun - no changes will be performed" , dest = 'dryrun' ,
146
+ action = 'store_true' , default = False ,
147
+ required = False )
148
+ args = parser .parse_args ()
149
+
150
+ log_level = logging .INFO
151
+
152
+ if args .verbose :
153
+ print ('Verbose logging selected' )
154
+ log_level = logging .DEBUG
155
+
156
+ logger = logging .getLogger ()
157
+ logger .setLevel (logging .DEBUG )
158
+ # # create file handler which logs even debug messages
159
+ # fh = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=5242880, backupCount=5)
160
+ # fh.setLevel(logging.DEBUG)
161
+ # create console handler using level set in log_level
162
+ ch = logging .StreamHandler ()
163
+ ch .setLevel (log_level )
164
+ console_formatter = logging .Formatter ('%(levelname)8s: %(message)s' )
165
+ ch .setFormatter (console_formatter )
166
+ # file_formatter = logging.Formatter('%(asctime)s - %(levelname)8s: %(message)s')
167
+ # fh.setFormatter(file_formatter)
168
+ # Add the handlers to the logger
169
+ # logger.addHandler(fh)
170
+ logger .addHandler (ch )
171
+
172
+ if not os .environ .get ('AWS_ACCESS_KEY_ID' ) and not args .aws_access_key :
173
+ logging .critical ('AWS Access Key Id not set - cannot continue' )
174
+
175
+ if not os .environ .get ('AWS_SECRET_ACCESS_KEY' ) and not args .aws_secret_key :
176
+ logging .critical ('AWS Secret Access Key not set - cannot continue' )
177
+
178
+ logging .debug ('INIT' )
179
+ logging .info ('Getting parameters with prefix %s from AWS Parameter Store' % args .param_prefix )
180
+ logging .info ('Parameter values will be compared against file contents in "%s" and updated if necessary' % args .cred_path )
181
+ process_parameters_with_prefix (args .param_prefix , args .cred_path , args .aws_region ,
182
+ args .aws_access_key , args .aws_secret_key , args .dryrun )
183
+ logging .info ('COMPLETE' )
0 commit comments