3737 default: none
3838 description:
3939 - identifier of key. Including this allows check mode to correctly report the changed state.
40+ - "If specifying a subkey's id be aware that apt-key does not understand how to remove keys via a subkey id. Specify the primary key's id instead."
4041 data:
4142 required: false
4243 default: none
4344 description:
44- - keyfile contents
45+ - keyfile contents to add to the keyring
4546 file:
4647 required: false
4748 default: none
4849 description:
49- - keyfile path
50+ - path to a keyfile to add to the keyring
5051 keyring:
5152 required: false
5253 default: none
106107
107108# FIXME: standardize into module_common
108109from traceback import format_exc
109- from re import compile as re_compile
110- # FIXME: standardize into module_common
111- from distutils .spawn import find_executable
112- from os import environ
113- from sys import exc_info
114- import traceback
115110
116- match_key = re_compile ("^gpg:.*key ([0-9a-fA-F]+):.*$" )
111+ from ansible .module_utils .basic import AnsibleModule
112+ from ansible .module_utils .urls import fetch_url
113+
114+
115+ apt_key_bin = None
116+
117+
118+ def find_needed_binaries (module ):
119+ global apt_key_bin
120+
121+ apt_key_bin = module .get_bin_path ('apt-key' , required = True )
122+
123+ ### FIXME: Is there a reason that gpg and grep are checked? Is it just
124+ # cruft or does the apt .deb package not require them (and if they're not
125+ # installed, /usr/bin/apt-key fails?)
126+ module .get_bin_path ('gpg' , required = True )
127+ module .get_bin_path ('grep' , required = True )
128+
129+
130+ def parse_key_id (key_id ):
131+ """validate the key_id and break it into segments
117132
118- REQUIRED_EXECUTABLES = ['gpg' , 'grep' , 'apt-key' ]
133+ :arg key_id: The key_id as supplied by the user. A valid key_id will be
134+ 8, 16, or more hexadecimal chars with an optional leading ``0x``.
135+ :returns: The portion of key_id suitable for apt-key del, the portion
136+ suitable for comparisons with --list-public-keys, and the portion that
137+ can be used with --recv-key. If key_id is long enough, these will be
138+ the last 8 characters of key_id, the last 16 characters, and all of
139+ key_id. If key_id is not long enough, some of the values will be the
140+ same.
119141
142+ * apt-key del <= 1.10 has a bug with key_id != 8 chars
143+ * apt-key adv --list-public-keys prints 16 chars
144+ * apt-key adv --recv-key can take more chars
145+
146+ """
147+ # Make sure the key_id is valid hexadecimal
148+ int (key_id , 16 )
149+
150+ key_id = key_id .upper ()
151+ if key_id .startswith ('0X' ):
152+ key_id = key_id [2 :]
153+
154+ key_id_len = len (key_id )
155+ if (key_id_len != 8 and key_id_len != 16 ) and key_id_len <= 16 :
156+ raise ValueError ('key_id must be 8, 16, or 16+ hexadecimal characters in length' )
157+
158+ short_key_id = key_id [- 8 :]
159+
160+ fingerprint = key_id
161+ if key_id_len > 16 :
162+ fingerprint = key_id [- 16 :]
163+
164+ return short_key_id , fingerprint , key_id
120165
121- def check_missing_binaries (module ):
122- missing = [e for e in REQUIRED_EXECUTABLES if not find_executable (e )]
123- if len (missing ):
124- module .fail_json (msg = "binaries are missing" , names = missing )
125166
126167def all_keys (module , keyring , short_format ):
127168 if keyring :
128- cmd = "apt-key --keyring %s adv --list-public-keys --keyid-format=long" % keyring
169+ cmd = "%s --keyring %s adv --list-public-keys --keyid-format=long" % ( apt_key_bin , keyring )
129170 else :
130- cmd = "apt-key adv --list-public-keys --keyid-format=long"
171+ cmd = "%s adv --list-public-keys --keyid-format=long" % apt_key_bin
131172 (rc , out , err ) = module .run_command (cmd )
132173 results = []
133174 lines = out .split ('\n ' )
134175 for line in lines :
135- if line .startswith ("pub" ):
176+ if line .startswith ("pub" ) or line . startswith ( "sub" ) :
136177 tokens = line .split ()
137178 code = tokens [1 ]
138179 (len_type , real_code ) = code .split ("/" )
@@ -141,6 +182,7 @@ def all_keys(module, keyring, short_format):
141182 results = shorten_key_ids (results )
142183 return results
143184
185+
144186def shorten_key_ids (key_id_list ):
145187 """
146188 Takes a list of key ids, and converts them to the 'short' format,
@@ -151,6 +193,7 @@ def shorten_key_ids(key_id_list):
151193 short .append (key [- 8 :])
152194 return short
153195
196+
154197def download_key (module , url ):
155198 # FIXME: move get_url code to common, allow for in-memory D/L, support proxies
156199 # and reuse here
@@ -166,52 +209,69 @@ def download_key(module, url):
166209 except Exception :
167210 module .fail_json (msg = "error getting key id from url: %s" % url , traceback = format_exc ())
168211
212+
169213def import_key (module , keyring , keyserver , key_id ):
170214 if keyring :
171- cmd = "apt-key --keyring %s adv --keyserver %s --recv %s" % (keyring , keyserver , key_id )
215+ cmd = "%s --keyring %s adv --keyserver %s --recv %s" % (apt_key_bin , keyring , keyserver , key_id )
172216 else :
173- cmd = "apt-key adv --keyserver %s --recv %s" % (keyserver , key_id )
174- (rc , out , err ) = module .run_command (cmd , check_rc = True )
217+ cmd = "%s adv --keyserver %s --recv %s" % (apt_key_bin , keyserver , key_id )
218+ for retry in range (5 ):
219+ lang_env = dict (LANG = 'C' , LC_ALL = 'C' , LC_MESSAGES = 'C' )
220+ (rc , out , err ) = module .run_command (cmd , environ_update = lang_env )
221+ if rc == 0 :
222+ break
223+ else :
224+ # Out of retries
225+ if rc == 2 and 'not found on keyserver' in out :
226+ msg = 'Key %s not found on keyserver %s' % (key_id , keyserver )
227+ module .fail_json (cmd = cmd , msg = msg )
228+ else :
229+ msg = "Error fetching key %s from keyserver: %s" % (key_id , keyserver )
230+ module .fail_json (cmd = cmd , msg = msg , rc = rc , stdout = out , stderr = err )
175231 return True
176232
233+
177234def add_key (module , keyfile , keyring , data = None ):
178235 if data is not None :
179236 if keyring :
180- cmd = "apt-key --keyring %s add -" % keyring
237+ cmd = "%s --keyring %s add -" % ( apt_key_bin , keyring )
181238 else :
182- cmd = "apt-key add -"
239+ cmd = "%s add -" % apt_key_bin
183240 (rc , out , err ) = module .run_command (cmd , data = data , check_rc = True , binary_data = True )
184241 else :
185242 if keyring :
186- cmd = "apt-key --keyring %s add %s" % (keyring , keyfile )
243+ cmd = "%s --keyring %s add %s" % (apt_key_bin , keyring , keyfile )
187244 else :
188- cmd = "apt-key add %s" % (keyfile )
245+ cmd = "%s add %s" % (apt_key_bin , keyfile )
189246 (rc , out , err ) = module .run_command (cmd , check_rc = True )
190247 return True
191248
249+
192250def remove_key (module , key_id , keyring ):
193251 # FIXME: use module.run_command, fail at point of error and don't discard useful stdin/stdout
194252 if keyring :
195- cmd = 'apt-key --keyring %s del %s' % (keyring , key_id )
253+ cmd = '%s --keyring %s del %s' % (apt_key_bin , keyring , key_id )
196254 else :
197- cmd = 'apt-key del %s' % key_id
255+ cmd = '%s del %s' % ( apt_key_bin , key_id )
198256 (rc , out , err ) = module .run_command (cmd , check_rc = True )
199257 return True
200258
259+
201260def main ():
202261 module = AnsibleModule (
203262 argument_spec = dict (
204263 id = dict (required = False , default = None ),
205264 url = dict (required = False ),
206265 data = dict (required = False ),
207- file = dict (required = False ),
266+ file = dict (required = False , type = 'path' ),
208267 key = dict (required = False ),
209- keyring = dict (required = False ),
268+ keyring = dict (required = False , type = 'path' ),
210269 validate_certs = dict (default = 'yes' , type = 'bool' ),
211270 keyserver = dict (required = False ),
212271 state = dict (required = False , choices = ['present' , 'absent' ], default = 'present' )
213272 ),
214- supports_check_mode = True
273+ supports_check_mode = True ,
274+ mutually_exclusive = (('filename' , 'keyserver' , 'data' , 'url' ),),
215275 )
216276
217277 key_id = module .params ['id' ]
@@ -223,64 +283,70 @@ def main():
223283 keyserver = module .params ['keyserver' ]
224284 changed = False
225285
226- # we use the "short" id: key_id[-8:], short_format=True
227- # it's a workaround for https://bugs.launchpad.net/ubuntu/+source/apt/+bug/1481871
228-
286+ fingerprint = short_key_id = key_id
287+ short_format = False
229288 if key_id :
230289 try :
231- _ = int (key_id , 16 )
232- if key_id .startswith ('0x' ):
233- key_id = key_id [2 :]
234- key_id = key_id .upper ()[- 8 :]
290+ short_key_id , fingerprint , key_id = parse_key_id (key_id )
235291 except ValueError :
236- module .fail_json (msg = "Invalid key_id" , id = key_id )
292+ module .fail_json (msg = 'Invalid key_id' , id = key_id )
293+
294+ if len (fingerprint ) == 8 :
295+ short_format = True
237296
238- # FIXME: I think we have a common facility for this, if not, want
239- check_missing_binaries (module )
297+ find_needed_binaries (module )
240298
241- short_format = True
242299 keys = all_keys (module , keyring , short_format )
243300 return_values = {}
244301
245302 if state == 'present' :
246- if key_id and key_id in keys :
303+ if fingerprint and fingerprint in keys :
247304 module .exit_json (changed = False )
305+ elif fingerprint and fingerprint not in keys and module .check_mode :
306+ ### TODO: Someday we could go further -- write keys out to
307+ # a temporary file and then extract the key id from there via gpg
308+ # to decide if the key is installed or not.
309+ module .exit_json (changed = True )
248310 else :
249311 if not filename and not data and not keyserver :
250312 data = download_key (module , url )
251- if key_id and key_id in keys :
252- module .exit_json (changed = False )
313+
314+ if filename :
315+ add_key (module , filename , keyring )
316+ elif keyserver :
317+ import_key (module , keyring , keyserver , key_id )
253318 else :
254- if module .check_mode :
255- module .exit_json (changed = True )
256- if filename :
257- add_key (module , filename , keyring )
258- elif keyserver :
259- import_key (module , keyring , keyserver , key_id )
260- else :
261- add_key (module , "-" , keyring , data )
262- changed = False
263- keys2 = all_keys (module , keyring , short_format )
264- if len (keys ) != len (keys2 ):
265- changed = True
266- if key_id and not key_id in keys2 :
267- module .fail_json (msg = "key does not seem to have been added" , id = key_id )
268- module .exit_json (changed = changed )
319+ add_key (module , "-" , keyring , data )
320+
321+ changed = False
322+ keys2 = all_keys (module , keyring , short_format )
323+ if len (keys ) != len (keys2 ):
324+ changed = True
325+
326+ if fingerprint and fingerprint not in keys2 :
327+ module .fail_json (msg = "key does not seem to have been added" , id = key_id )
328+ module .exit_json (changed = changed )
329+
269330 elif state == 'absent' :
270331 if not key_id :
271332 module .fail_json (msg = "key is required" )
272- if key_id in keys :
333+ if fingerprint in keys :
273334 if module .check_mode :
274335 module .exit_json (changed = True )
275- if remove_key (module , key_id , keyring ):
276- changed = True
336+
337+ # we use the "short" id: key_id[-8:], short_format=True
338+ # it's a workaround for https://bugs.launchpad.net/ubuntu/+source/apt/+bug/1481871
339+ if remove_key (module , short_key_id , keyring ):
340+ keys = all_keys (module , keyring , short_format )
341+ if fingerprint in keys :
342+ module .fail_json (msg = "apt-key del did not return an error but the key was not removed (check that the id is correct and *not* a subkey)" , id = key_id )
343+ changed = True
277344 else :
278- # FIXME: module.fail_json or exit-json immediately at point of failure
345+ # FIXME: module.fail_json or exit-json immediately at point of failure
279346 module .fail_json (msg = "error removing key_id" , ** return_values )
280347
281348 module .exit_json (changed = changed , ** return_values )
282349
283- # import module snippets
284- from ansible .module_utils .basic import *
285- from ansible .module_utils .urls import *
286- main ()
350+
351+ if __name__ == '__main__' :
352+ main ()
0 commit comments