11from __future__ import print_function
22from __future__ import division
33from __future__ import absolute_import
4- from builtins import input
4+
55import logging
66import os
7+ import sys
8+
9+ from botocore .exceptions import ClientError
710
811from stacker .session_cache import get_session
12+ from stacker .hooks import utils
13+ from stacker .ui import get_raw_input
914
10- from . import utils
1115
1216logger = logging .getLogger (__name__ )
1317
18+ KEYPAIR_LOG_MESSAGE = "keypair: %s (%s) %s"
19+
20+
21+ def get_existing_key_pair (ec2 , keypair_name ):
22+ resp = ec2 .describe_key_pairs ()
23+ keypair = next ((kp for kp in resp ["KeyPairs" ]
24+ if kp ["KeyName" ] == keypair_name ), None )
25+
26+ if keypair :
27+ logger .info (KEYPAIR_LOG_MESSAGE ,
28+ keypair ["KeyName" ],
29+ keypair ["KeyFingerprint" ],
30+ "exists" )
31+ return {
32+ "status" : "exists" ,
33+ "key_name" : keypair ["KeyName" ],
34+ "fingerprint" : keypair ["KeyFingerprint" ],
35+ }
1436
15- def find (lst , key , value ):
16- for i , dic in enumerate (lst ):
17- if dic [key ] == value :
18- return lst [i ]
19- return False
37+ logger .info ("keypair: \" %s\" not found" , keypair_name )
38+ return None
39+
40+
41+ def import_key_pair (ec2 , keypair_name , public_key_data ):
42+ keypair = ec2 .import_key_pair (
43+ KeyName = keypair_name ,
44+ PublicKeyMaterial = public_key_data .strip (),
45+ DryRun = False )
46+ logger .info (KEYPAIR_LOG_MESSAGE ,
47+ keypair ["KeyName" ],
48+ keypair ["KeyFingerprint" ],
49+ "imported" )
50+ return keypair
51+
52+
53+ def read_public_key_file (path ):
54+ try :
55+ with open (utils .full_path (path ), 'rb' ) as f :
56+ data = f .read ()
57+
58+ if not data .startswith (b"ssh-rsa" ):
59+ raise ValueError (
60+ "Bad public key data, must be an RSA key in SSH authorized "
61+ "keys format (beginning with `ssh-rsa`)" )
62+
63+ return data .strip ()
64+ except (ValueError , IOError , OSError ) as e :
65+ logger .error ("Failed to read public key file {}: {}" .format (
66+ path , e ))
67+ return None
68+
69+
70+ def create_key_pair_from_public_key_file (ec2 , keypair_name , public_key_path ):
71+ public_key_data = read_public_key_file (public_key_path )
72+ if not public_key_data :
73+ return None
74+
75+ keypair = import_key_pair (ec2 , keypair_name , public_key_data )
76+ return {
77+ "status" : "imported" ,
78+ "key_name" : keypair ["KeyName" ],
79+ "fingerprint" : keypair ["KeyFingerprint" ],
80+ }
81+
82+
83+ def create_key_pair_in_ssm (ec2 , ssm , keypair_name , parameter_name ,
84+ kms_key_id = None ):
85+ keypair = create_key_pair (ec2 , keypair_name )
86+ try :
87+ kms_key_label = 'default'
88+ kms_args = {}
89+ if kms_key_id :
90+ kms_key_label = kms_key_id
91+ kms_args = {"KeyId" : kms_key_id }
92+
93+ logger .info ("Storing generated key in SSM parameter \" %s\" "
94+ "using KMS key \" %s\" " , parameter_name , kms_key_label )
95+
96+ ssm .put_parameter (
97+ Name = parameter_name ,
98+ Description = "SSH private key for KeyPair \" {}\" "
99+ "(generated by Stacker)" .format (keypair_name ),
100+ Value = keypair ["KeyMaterial" ],
101+ Type = "SecureString" ,
102+ Overwrite = False ,
103+ ** kms_args )
104+ except ClientError :
105+ # Erase the key pair if we failed to store it in SSM, since the
106+ # private key will be lost anyway
107+
108+ logger .exception ("Failed to store generated key in SSM, deleting "
109+ "created key pair as private key will be lost" )
110+ ec2 .delete_key_pair (KeyName = keypair_name , DryRun = False )
111+ return None
112+
113+ return {
114+ "status" : "created" ,
115+ "key_name" : keypair ["KeyName" ],
116+ "fingerprint" : keypair ["KeyFingerprint" ],
117+ }
118+
119+
120+ def create_key_pair (ec2 , keypair_name ):
121+ keypair = ec2 .create_key_pair (KeyName = keypair_name , DryRun = False )
122+ logger .info (KEYPAIR_LOG_MESSAGE ,
123+ keypair ["KeyName" ],
124+ keypair ["KeyFingerprint" ],
125+ "created" )
126+ return keypair
127+
128+
129+ def create_key_pair_local (ec2 , keypair_name , dest_dir ):
130+ dest_dir = utils .full_path (dest_dir )
131+ if not os .path .isdir (dest_dir ):
132+ logger .error ("\" %s\" is not a valid directory" , dest_dir )
133+ return None
134+
135+ file_name = "{0}.pem" .format (keypair_name )
136+ key_path = os .path .join (dest_dir , file_name )
137+ if os .path .isfile (key_path ):
138+ # This mimics the old boto2 keypair.save error
139+ logger .error ("\" %s\" already exists in \" %s\" directory" ,
140+ file_name , dest_dir )
141+ return None
142+
143+ # Open the file before creating the key pair to catch errors early
144+ with open (key_path , "wb" ) as f :
145+ keypair = create_key_pair (ec2 , keypair_name )
146+ f .write (keypair ["KeyMaterial" ].encode ("ascii" ))
147+
148+ return {
149+ "status" : "created" ,
150+ "key_name" : keypair ["KeyName" ],
151+ "fingerprint" : keypair ["KeyFingerprint" ],
152+ "file_path" : key_path
153+ }
154+
155+
156+ def interactive_prompt (keypair_name , ):
157+ if not sys .stdin .isatty ():
158+ return None , None
159+
160+ try :
161+ while True :
162+ action = get_raw_input (
163+ "import or create keypair \" %s\" ? (import/create/cancel) " % (
164+ keypair_name ,
165+ )
166+ )
167+
168+ if action .lower () == "cancel" :
169+ break
170+
171+ if action .lower () in ("i" , "import" ):
172+ path = get_raw_input ("path to keypair file: " )
173+ return "import" , path .strip ()
174+
175+ if action .lower () == "create" :
176+ path = get_raw_input ("directory to save keyfile: " )
177+ return "create" , path .strip ()
178+ except (EOFError , KeyboardInterrupt ):
179+ return None , None
180+
181+ return None , None
20182
21183
22184def ensure_keypair_exists (provider , context , ** kwargs ):
@@ -28,84 +190,63 @@ def ensure_keypair_exists(provider, context, **kwargs):
28190 provider (:class:`stacker.providers.base.BaseProvider`): provider
29191 instance
30192 context (:class:`stacker.context.Context`): context instance
31-
32- Returns: boolean for whether or not the hook succeeded.
193+ keypair (str): name of the key pair to create
194+ ssm_parameter_name (str, optional): path to an SSM store parameter to
195+ receive the generated private key, instead of importing it or
196+ storing it locally.
197+ ssm_key_id (str, optional): ID of a KMS key to encrypt the SSM
198+ parameter with. If omitted, the default key will be used.
199+ public_key_path (str, optional): path to a public key file to be
200+ imported instead of generating a new key. Incompatible with the SSM
201+ options, as the private key will not be available for storing.
202+
203+ Returns:
204+ In case of failure ``False``, otherwise a dict containing:
205+ status (str): one of "exists", "imported" or "created"
206+ key_name (str): name of the key pair
207+ fingerprint (str): fingerprint of the key pair
208+ file_path (str, optional): if a new key was created, the path to
209+ the file where the private key was stored
33210
34211 """
35- session = get_session (provider .region )
36- client = session .client ("ec2" )
37- keypair_name = kwargs .get ("keypair" )
38- resp = client .describe_key_pairs ()
39- keypair = find (resp ["KeyPairs" ], "KeyName" , keypair_name )
40- message = "keypair: %s (%s) %s"
212+
213+ keypair_name = kwargs ["keypair" ]
214+ ssm_parameter_name = kwargs .get ("ssm_parameter_name" )
215+ ssm_key_id = kwargs .get ("ssm_key_id" )
216+ public_key_path = kwargs .get ("public_key_path" )
217+
218+ if public_key_path and ssm_parameter_name :
219+ logger .error ("public_key_path and ssm_parameter_name cannot be "
220+ "specified at the same time" )
221+ return False
222+
223+ session = get_session (region = provider .region ,
224+ profile = kwargs .get ("profile" ))
225+ ec2 = session .client ("ec2" )
226+
227+ keypair = get_existing_key_pair (ec2 , keypair_name )
41228 if keypair :
42- logger .info (message ,
43- keypair ["KeyName" ],
44- keypair ["KeyFingerprint" ],
45- "exists" )
46- return {
47- "status" : "exists" ,
48- "key_name" : keypair ["KeyName" ],
49- "fingerprint" : keypair ["KeyFingerprint" ],
50- }
229+ return keypair
51230
52- logger .info ("keypair: \" %s\" not found" , keypair_name )
53- create_or_upload = input (
54- "import or create keypair \" %s\" ? (import/create/Cancel) " % (
55- keypair_name ,
56- ),
57- )
58- if create_or_upload == "import" :
59- path = input ("path to keypair file: " )
60- full_path = utils .full_path (path )
61- if not os .path .exists (full_path ):
62- logger .error ("Failed to find keypair at path: %s" , full_path )
63- return False
64-
65- with open (full_path ) as read_file :
66- contents = read_file .read ()
67-
68- keypair = client .import_key_pair (KeyName = keypair_name ,
69- PublicKeyMaterial = contents )
70- logger .info (message ,
71- keypair ["KeyName" ],
72- keypair ["KeyFingerprint" ],
73- "imported" )
74- return {
75- "status" : "imported" ,
76- "key_name" : keypair ["KeyName" ],
77- "fingerprint" : keypair ["KeyFingerprint" ],
78- "file_path" : full_path ,
79- }
80- elif create_or_upload == "create" :
81- path = input ("directory to save keyfile: " )
82- full_path = utils .full_path (path )
83- if not os .path .exists (full_path ) and not os .path .isdir (full_path ):
84- logger .error ("\" %s\" is not a valid directory" , full_path )
85- return False
86-
87- file_name = "{0}.pem" .format (keypair_name )
88- if os .path .isfile (os .path .join (full_path , file_name )):
89- # This mimics the old boto2 keypair.save error
90- logger .error ("\" %s\" already exists in \" %s\" directory" ,
91- file_name ,
92- full_path )
93- return False
94-
95- keypair = client .create_key_pair (KeyName = keypair_name )
96- logger .info (message ,
97- keypair ["KeyName" ],
98- keypair ["KeyFingerprint" ],
99- "created" )
100- with open (os .path .join (full_path , file_name ), "w" ) as f :
101- f .write (keypair ["KeyMaterial" ])
231+ if public_key_path :
232+ keypair = create_key_pair_from_public_key_file (
233+ ec2 , keypair_name , public_key_path )
102234
103- return {
104- "status" : "created" ,
105- "key_name" : keypair ["KeyName" ],
106- "fingerprint" : keypair ["KeyFingerprint" ],
107- "file_path" : os .path .join (full_path , file_name )
108- }
235+ elif ssm_parameter_name :
236+ ssm = session .client ('ssm' )
237+ keypair = create_key_pair_in_ssm (
238+ ec2 , ssm , keypair_name , ssm_parameter_name , ssm_key_id )
109239 else :
110- logger .warning ("no action to find keypair, failing" )
240+ action , path = interactive_prompt (keypair_name )
241+ if action == "import" :
242+ keypair = create_key_pair_from_public_key_file (
243+ ec2 , keypair_name , path )
244+ elif action == "create" :
245+ keypair = create_key_pair_local (ec2 , keypair_name , path )
246+ else :
247+ logger .warning ("no action to find keypair, failing" )
248+
249+ if not keypair :
111250 return False
251+
252+ return keypair
0 commit comments