Repository: ansible/ansible-modules-extras

Issue:
known_hosts module should create missing .ssh dir
I have this in my playbook:

``` yaml
- name: Make sure Bitbucket is a known host
  known_hosts:
    host: 'bitbucket.org'
    key: 'bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw=='
```

When I run this playbook, I get this output:

```
Romans-MacBook-Pro.local ➜  xanview-ansible  time ansible-playbook timeboxes.yml --tag timeagent

PLAY [vl] ********************************************************************* 

GATHERING FACTS *************************************************************** 
ok: [vl]

TASK: [timeagent | Make sure Bitbucket is a known host] *********************** 
failed: [vl] => {"failed": true, "parsed": false}
BECOME-SUCCESS-ksmduaugpgtrnsanzdmnlxfuctzmlprk
OpenSSH_6.2p2, OSSLShim 0.9.8r 8 Dec 2011
debug1: Reading configuration data /etc/ssh_config
debug1: /etc/ssh_config line 20: Applying options for *
debug1: /etc/ssh_config line 102: Applying options for *
debug1: Control socket "/Users/Hackeron/.ansible/cp/ansible-ssh-vpn.vl.zanview.com-22-deployer" does not exist
debug1: Connecting to vpn.vl.zanview.com [10.8.0.14] port 22.
debug1: fd 3 clearing O_NONBLOCK
debug1: Connection established.
debug1: identity file /Users/Hackeron/.ssh/id_rsa type 1
debug1: identity file /Users/Hackeron/.ssh/id_rsa-cert type -1
debug1: identity file /Users/Hackeron/.ssh/id_dsa type -1
debug1: identity file /Users/Hackeron/.ssh/id_dsa-cert type -1
debug1: Enabling compatibility mode for protocol 2.0
debug1: Local version string SSH-2.0-OpenSSH_6.2
debug1: Remote protocol version 2.0, remote software version OpenSSH_6.6.1p1 Ubuntu-8
debug1: match: OpenSSH_6.6.1p1 Ubuntu-8 pat OpenSSH*
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: server->client aes128-ctr hmac-md5-etm@openssh.com zlib@openssh.com
debug1: kex: client->server aes128-ctr hmac-md5-etm@openssh.com zlib@openssh.com
debug1: SSH2_MSG_KEX_DH_GEX_REQUEST(1024<1024<8192) sent
debug1: expecting SSH2_MSG_KEX_DH_GEX_GROUP
debug1: SSH2_MSG_KEX_DH_GEX_INIT sent
debug1: expecting SSH2_MSG_KEX_DH_GEX_REPLY
debug1: Server host key: RSA a0:b0:35:6e:9e:3a:fe:4e:be:dc:82:f2:4a:a1:41:9b
debug1: Host 'vpn.vl.zanview.com' is known and matches the RSA host key.
debug1: Found key in /Users/Hackeron/.ssh/known_hosts:95
Warning: the RSA host key for 'vpn.vl.zanview.com' differs from the key for the IP address '10.8.0.14'
Offending key for IP in /Users/Hackeron/.ssh/known_hosts:47
Matching host key in /Users/Hackeron/.ssh/known_hosts:95
debug1: ssh_rsa_verify: signature correct
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug1: Roaming not allowed by server
debug1: SSH2_MSG_SERVICE_REQUEST sent
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: publickey
debug1: Offering RSA public key: /Users/Hackeron/.ssh/id_rsa
debug1: Server accepts key: pkalg ssh-rsa blen 279
debug1: Enabling compression at level 6.
debug1: Authentication succeeded (publickey).
Authenticated to vpn.vl.zanview.com ([10.8.0.14]:22).
debug1: channel 0: new [client-session]
debug1: Requesting no-more-sessions@openssh.com
debug1: Entering interactive session.
debug1: Requesting authentication agent forwarding.
debug1: Sending environment.
debug1: Sending env LANG = en_GB.UTF-8
debug1: Sending env LC_CTYPE = en_GB.UTF-8
debug1: Sending command: /bin/sh -c 'sudo -k && sudo -H -S -p "[sudo via ansible, key=ksmduaugpgtrnsanzdmnlxfuctzmlprk] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-ksmduaugpgtrnsanzdmnlxfuctzmlprk; LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /usr/bin/python'"'"''
Traceback (most recent call last):
  File "<stdin>", line 1835, in <module>
  File "<stdin>", line 236, in main
  File "<stdin>", line 132, in enforce_state
  File "/usr/lib/python2.7/tempfile.py", line 462, in NamedTemporaryFile
    (fd, name) = _mkstemp_inner(dir, prefix, suffix, flags)
  File "/usr/lib/python2.7/tempfile.py", line 239, in _mkstemp_inner
    fd = _os.open(file, flags, 0600)
OSError: [Errno 2] No such file or directory: '/root/.ssh/tmpkNjfhi'
debug1: client_input_channel_req: channel 0 rtype exit-status reply 0
debug1: channel 0: free: client-session, nchannels 1
debug1: fd 0 clearing O_NONBLOCK
debug1: fd 1 clearing O_NONBLOCK
debug1: fd 2 clearing O_NONBLOCK
Transferred: sent 22712, received 3640 bytes, in 3.3 seconds
Bytes per second: sent 6941.3, received 1112.5
debug1: Exit status 1
debug1: compress outgoing: raw data 71245, compressed 19631, factor 0.28
debug1: compress incoming: raw data 705, compressed 477, factor 0.68


FATAL: all hosts have already failed -- aborting

PLAY RECAP ******************************************************************** 
           to retry, use: --limit @/Users/Hackeron/timeboxes.retry
```

Any ideas?



Discussion:
That's outf=tempfile.NamedTemporaryFile(dir=os.path.dirname(path),delete=False) failing.

Are you trying to update a known_hosts file in a directory that either a) doesn't exist b) you can't write to?

it seems that /root/.ssh does not exist as you do successfully sudo to root.

yeh, I think that's it! - shouldn't it create the .ssh directory if it is missing?

what if the home is missing also?

@hackeron I don't think I agree - I think the general unix philosophy is that the user should create directories to put things in.
For example:
mcv21@pick:~$ touch nonexistant/foo
touch: cannot touch `nonexistant/foo': No such file or directory

I think creating the .ssh folder is relevant to the function of adding a known_host. Do you not agree?

Creating a home folder on the other hand, is outside of the function of this module.

Also maybe the error should be something more description rather than a scary exception in any case?

I'm not entirely convinced, but I would be content with "create containing directory if and only if that is ~/.ssh". Would that suit you?

[FWIW, you might rather populate the system-wide /etc/ssh/ssh_known_hosts rather than /root/.ssh/known_hosts]

I think creating just the .ssh directory makes sense? - So long as ssh is installed, it seems /etc/ssh is created anyway.

As for if the /etc/ssh directory is missing, Ansible should show a more helpful error rather than raise an exception and show all that connection debugging stuff. What do you think?

yes, the exception is always a bug, keeping this open to decide if we should create .ssh or not, but we should definitely handle the exception.

PR just submitted creates containing directory iff ~/.ssh, otherwise emits a tidy error message.

might be too limited, then there will be the complaint that it doesnt do same for path=/home/otheruser/.ssh/known_hosts

I don't think creating .ssh is a good idea seeing as path could specify anywhere.  It should just fail gracefully with a message if your path does not exist.  It's easy enough to add another ansible step in your playbook pre the call to this module to create the directory.  I always think that modules should do exactly what they say on the tin (in this case, add a known host) and not start to presume and do other things (such as create a directory)

reclassified as bug cause of the traceback, not adding .ssh dir creation


Resolving patch:
diff --git a/system/known_hosts.py b/system/known_hosts.py
--- a/system/known_hosts.py
+++ b/system/known_hosts.py
@@ -137,11 +137,11 @@ def enforce_state(module, params):
             outf.write(key)
             outf.close()
             module.atomic_move(outf.name,path)
-        except IOError,e:
+        except (IOError,OSError),e:
             module.fail_json(msg="Failed to write to file %s: %s" % \
                                  (path,str(e)))
         params['changed'] = True
-    
+
     return params
 
 def sanity_check(module,host,key,sshkeygen):


Test patch: