11#!/usr/bin/env python3
2+ """
3+ SSH to a cluster host using connection properties from Ansible inventory.
24
3- # This tool allows you to ssh into a host using the ansible inventory.
4- # Example: ansible-ssh compute[0] -o GlobalKnownHostsFile=/dev/null -o
5- # UserKnownHostsFile=/dev/null
5+ Usage:
6+ ansible-ssh [-n] PATTERN
7+
8+ where PATTERN can be any of:
9+ - host e.g. mycluster-login-0
10+ - group e.g. login
11+ - group[index] e.g. compute[0]
12+ options:
13+ -n Disable strict host key checking and do not store accepted keys
14+ """
615
716import json
817import os
@@ -11,55 +20,44 @@ import subprocess
1120import sys
1221from collections import defaultdict
1322
14-
15- def _optional_arg (prototype , * values ):
16- # returns empty string if any of the values are falsey
17- filtered = [value for value in values if value ]
18- return prototype .format (* values ) if len (values ) == len (filtered ) else ""
19-
23+ NO_HOSTKEY_CHECK_OPTS = [
24+ '-o' , 'StrictHostKeyChecking=no' ,
25+ '-o' , 'GlobalKnownHostsFile=/dev/null' ,
26+ '-o' , 'UserKnownHostsFile=/dev/null'
27+ ]
2028
2129if __name__ == "__main__" :
22- if len (sys .argv ) < 2 :
23- msg = (
24- f"Usage: { sys .argv [0 ]} <inventory_hostname> [args to pass to ssh]" )
25- print (msg , file = sys .stderr )
30+ no_known_hosts = '-n' in sys .argv
31+ if sys .argv [- 1 ] in (sys .argv [0 ], '-n' ):
32+ print (__doc__ , file = sys .stderr )
2633 sys .exit (- 1 )
2734
28- # Quote to prevent shell injection
29- host = shlex .quote (sys .argv [1 ])
35+ pattern = sys .argv [- 1 ]
3036
37+ template_str = ("ssh "
38+ "{% if ansible_ssh_port | default(false) %}-p {{ ansible_ssh_port }} {% endif %}"
39+ "{% if ansible_ssh_private_key_file | default(false) %} -i {{ ansible_ssh_private_key_file }} {% endif %}"
40+ "{{ ansible_ssh_common_args | default('') }} "
41+ "{{ ansible_user }}@{{ ansible_host }}" )
42+ module_args = json .dumps ({'msg' :template_str })
43+ ansible_cmd = ['ansible' , pattern , '-o' , '-m' , 'debug' , '-a' , module_args ]
3144 try :
32- output = subprocess .check_output (
33- f'ansible-inventory --host { host } ' , shell = True )
45+ output = subprocess .check_output (ansible_cmd , text = True )
3446 except (subprocess .CalledProcessError ) as e :
35- msg = (f"[ERROR]: Is { host } missing from the inventory?" )
36- print (msg , file = sys .stderr )
37- sys .exit (- 1 )
38-
39- meta = defaultdict (str , json .loads (output ))
40-
41- ansible_ssh_host = meta ['ansible_ssh_host' ] or meta ['ansible_host' ]
42- ansible_ssh_user = meta ['ansible_ssh_user' ] or meta ['ansible_user' ]
43- ansible_ssh_port = meta ['ansible_ssh_port' ]
44- ansible_ssh_private_key_file = meta ['ansible_ssh_private_key_file' ]
45-
46- port = _optional_arg ("-p {}" , ansible_ssh_port )
47- identity = _optional_arg ("-i {}" , ansible_ssh_private_key_file )
48- host = _optional_arg ("{}@{}" , ansible_ssh_user , ansible_ssh_host )
49- opts = meta ['ansible_ssh_common_args' ]
50-
51- # Handle case where user is not set
52- if not host :
53- host = ansible_ssh_host
54-
55- if not host :
56- # if we get here, "ansible_ssh_host" is not set.
57- msg = f"Could not determine the host"
47+ msg = (f"[ERROR]: Is { pattern } missing from the inventory?" )
5848 print (msg , file = sys .stderr )
5949 sys .exit (- 1 )
50+ # output looks like e.g.
51+ # stg-login-0 | SUCCESS => { "changed": false, ...
52+ # one line per host
53+ result = output .splitlines ()[0 ].split ('>' , 1 )[- 1 ]
54+ expanded = json .loads (result )['msg' ]
55+ # can assume ansible_host exists b/c defined by terraform
56+ # can assume ansible_user exists b/c defined in common inventory
57+
58+ cmd = shlex .split (expanded )
59+ if no_known_hosts :
60+ cmd = cmd [0 :1 ] + NO_HOSTKEY_CHECK_OPTS + cmd [1 :]
6061
61- base = shlex .split (f'ssh { port } { identity } { opts } ' )
62- extras = sys .argv [2 :]
63- cmd = base + extras + [host ]
6462 print (f"[INFO]: Running: { subprocess .list2cmdline (cmd )} " )
6563 os .execvp (cmd [0 ], cmd )
0 commit comments