@ -23,7 +23,7 @@ DOCUMENTATION = '''
module : known_hosts
module : known_hosts
short_description : Add or remove a host from the C ( known_hosts ) file
short_description : Add or remove a host from the C ( known_hosts ) file
description :
description :
- The M ( known_hosts ) module lets you add or remove a host from the C ( known_hosts ) file .
- The M ( known_hosts ) module lets you add or remove a host keys from the C ( known_hosts ) file . Multiple entries per host are allowed , but only one for each key type supported by ssh .
This is useful if you ' re going to want to use the M(git) module over ssh, for example.
This is useful if you ' re going to want to use the M(git) module over ssh, for example.
If you have a very large number of host keys to manage , you will find the M ( template ) module more useful .
If you have a very large number of host keys to manage , you will find the M ( template ) module more useful .
version_added : " 1.9 "
version_added : " 1.9 "
@ -36,7 +36,7 @@ options:
default : null
default : null
key :
key :
description :
description :
- The SSH public host key , as a string ( required if state = present , optional when state = absent , in which case all keys for the host are removed )
- The SSH public host key , as a string ( required if state = present , optional when state = absent , in which case all keys for the host are removed ) . The key must be in the right format for ssh ( see ssh ( 1 ) , section " SSH_KNOWN_HOSTS FILE FORMAT " )
required : false
required : false
default : null
default : null
path :
path :
@ -46,7 +46,7 @@ options:
default : " (homedir)+/.ssh/known_hosts "
default : " (homedir)+/.ssh/known_hosts "
state :
state :
description :
description :
- I ( present ) to add the host , I ( absent ) to remove it .
- I ( present ) to add the host key , I ( absent ) to remove it .
choices : [ " present " , " absent " ]
choices : [ " present " , " absent " ]
required : no
required : no
default : present
default : present
@ -76,6 +76,7 @@ import os
import os . path
import os . path
import tempfile
import tempfile
import errno
import errno
import re
def enforce_state ( module , params ) :
def enforce_state ( module , params ) :
"""
"""
@ -99,24 +100,24 @@ def enforce_state(module, params):
sanity_check ( module , host , key , sshkeygen )
sanity_check ( module , host , key , sshkeygen )
current, replac e= search_for_host_key ( module , host , key , path , sshkeygen )
found, replace_or_add , found_lin e= search_for_host_key ( module , host , key , path , sshkeygen )
#We will change state if current ==True & state!="present"
#We will change state if found ==True & state!="present"
#or current ==False & state=="present"
#or found ==False & state=="present"
#i.e (current) XOR (state=="present")
#i.e found XOR (state=="present")
#Alternatively, if replace is true (i.e. key present, and we must change it)
#Alternatively, if replace is true (i.e. key present, and we must change it)
if module . check_mode :
if module . check_mode :
module . exit_json ( changed = replace or ( ( state == " present " ) != current) )
module . exit_json ( changed = replace _or_add or ( state == " present " ) != found )
#Now do the work.
#Now do the work.
#First, remove an extant entry if required
#Only remove whole host if found and no key provided
if replace == True or ( current == True and state == " absent " ) :
if found and key is None and state == " absent " :
module . run_command ( [ sshkeygen , ' -R ' , host , ' -f ' , path ] ,
module . run_command ( [ sshkeygen , ' -R ' , host , ' -f ' , path ] , check_rc = True )
check_rc = True )
params [ ' changed ' ] = True
params [ ' changed ' ] = True
#Next, add a new (or replacing) entry
#Next, add a new (or replacing) entry
if replace == True or ( current == False and state == " present " ) :
if replace _or_add or found != ( state == " present " ) :
try :
try :
inf = open ( path , " r " )
inf = open ( path , " r " )
except IOError , e :
except IOError , e :
@ -128,10 +129,13 @@ def enforce_state(module, params):
try :
try :
outf = tempfile . NamedTemporaryFile ( dir = os . path . dirname ( path ) )
outf = tempfile . NamedTemporaryFile ( dir = os . path . dirname ( path ) )
if inf is not None :
if inf is not None :
for line in inf :
for line_number , line in enumerate ( inf , start = 1 ) :
if found_line == line_number and ( replace_or_add or state == ' absent ' ) :
continue # skip this line to replace its key
outf . write ( line )
outf . write ( line )
inf . close ( )
inf . close ( )
outf . write ( key )
if state == ' present ' :
outf . write ( key )
outf . flush ( )
outf . flush ( )
module . atomic_move ( outf . name , path )
module . atomic_move ( outf . name , path )
except ( IOError , OSError ) , e :
except ( IOError , OSError ) , e :
@ -183,54 +187,76 @@ def sanity_check(module,host,key,sshkeygen):
module . fail_json ( msg = " Host parameter does not match hashed host field in supplied key " )
module . fail_json ( msg = " Host parameter does not match hashed host field in supplied key " )
def search_for_host_key ( module , host , key , path , sshkeygen ) :
def search_for_host_key ( module , host , key , path , sshkeygen ) :
''' search_for_host_key(module,host,key,path,sshkeygen) -> ( current,replac e)
''' search_for_host_key(module,host,key,path,sshkeygen) -> ( found,replace_or_add,found_lin e)
Looks up host in the known_hosts file path ; if it ' s there, looks to see
Looks up host and keytype in the known_hosts file path ; if it ' s there, looks to see
if one of those entries matches key . Returns :
if one of those entries matches key . Returns :
current ( Boolean ) : is host found in path ?
found ( Boolean ) : is host found in path ?
replace ( Boolean ) : is the key in path different to that supplied by user ?
replace_or_add ( Boolean ) : is the key in path different to that supplied by user ?
if current = False , then replace is always False .
found_line ( int or None ) : the line where a key of the same type was found
if found = False , then replace is always False .
sshkeygen is the path to ssh - keygen , found earlier with get_bin_path
sshkeygen is the path to ssh - keygen , found earlier with get_bin_path
'''
'''
replace = False
if os . path . exists ( path ) == False :
if os . path . exists ( path ) == False :
return False , False
return False , False , None
#openssh >=6.4 has changed ssh-keygen behaviour such that it returns
#openssh >=6.4 has changed ssh-keygen behaviour such that it returns
#1 if no host is found, whereas previously it returned 0
#1 if no host is found, whereas previously it returned 0
rc , stdout , stderr = module . run_command ( [ sshkeygen , ' -F ' , host , ' -f ' , path ] ,
rc , stdout , stderr = module . run_command ( [ sshkeygen , ' -F ' , host , ' -f ' , path ] ,
check_rc = False )
check_rc = False )
if stdout == ' ' and stderr == ' ' and ( rc == 0 or rc == 1 ) :
if stdout == ' ' and stderr == ' ' and ( rc == 0 or rc == 1 ) :
return False , False #host not found, no other errors
return False , False , None #host not found, no other errors
if rc != 0 : #something went wrong
if rc != 0 : #something went wrong
module . fail_json ( msg = " ssh-keygen failed (rc= %d ,stdout= ' %s ' ,stderr= ' %s ' ) " % ( rc , stdout , stderr ) )
module . fail_json ( msg = " ssh-keygen failed (rc= %d ,stdout= ' %s ' ,stderr= ' %s ' ) " % ( rc , stdout , stderr ) )
#If user supplied no key, we don't want to try and replace anything with it
#If user supplied no key, we don't want to try and replace anything with it
if key is None :
if key is None :
return True , False
return True , False , None
lines = stdout . split ( ' \n ' )
lines = stdout . split ( ' \n ' )
k = key . strip ( ) #trim trailing newline
new_key = normalize_known_hosts_key ( key , host )
#ssh-keygen returns only the host we ask about in the host field,
#even if the key entry has multiple hosts. Emulate this behaviour here,
#otherwise we get false negatives.
#Only necessary for unhashed entries.
if k [ 0 ] != ' | ' :
k = k . split ( )
#The optional "marker" field, used for @cert-authority or @revoked
if k [ 0 ] [ 0 ] == ' @ ' :
k [ 1 ] = host
else :
k [ 0 ] = host
k = ' ' . join ( k )
for l in lines :
for l in lines :
if l == ' ' :
if l == ' ' :
continue
continue
if l [ 0 ] == ' # ' : #comment
elif l [ 0 ] == ' # ' : # info output from ssh-keygen; contains the line number where key was found
continue
try :
if k == l : #found a match
# This output format has been hardcoded in ssh-keygen since at least OpenSSH 4.0
return True , False #current, not-replace
# It always outputs the non-localized comment before the found key
#No match found, return current and replace
found_line = int ( re . search ( r ' found: line ( \ d+) ' , l ) . group ( 1 ) )
return True , True
except IndexError , e :
module . fail_json ( msg = " failed to parse output of ssh-keygen for line number: ' %s ' " % l )
else :
found_key = normalize_known_hosts_key ( l , host )
if new_key == found_key : #found a match
return True , False , found_line #found exactly the same key, don't replace
elif new_key [ ' type ' ] == found_key [ ' type ' ] : # found a different key for the same key type
return True , True , found_line
#No match found, return found and replace, but no line
return True , True , None
def normalize_known_hosts_key ( key , host ) :
'''
Transform a key , either taken from a known_host file or provided by the
user , into a normalized form .
The host part ( which might include multiple hostnames or be hashed ) gets
replaced by the provided host . Also , any spurious information gets removed
from the end ( like the username @host tag usually present in hostkeys , but
absent in known_hosts files )
'''
k = key . strip ( ) #trim trailing newline
k = key . split ( )
d = dict ( )
#The optional "marker" field, used for @cert-authority or @revoked
if k [ 0 ] [ 0 ] == ' @ ' :
d [ ' options ' ] = k [ 0 ]
d [ ' host ' ] = host
d [ ' type ' ] = k [ 2 ]
d [ ' key ' ] = k [ 3 ]
else :
d [ ' host ' ] = host
d [ ' type ' ] = k [ 1 ]
d [ ' key ' ] = k [ 2 ]
return d
def main ( ) :
def main ( ) :