Newer
Older
"""
This module handles SSH Connections
"""
import subprocess
class SshAgentException(Exception):
pass
class Ssh(object):
"""
Ssh class can execute or create tunnelstat
"""
@staticmethod
def execute(sess, host, user, cmd, stdin=None):
"""
execute the command cmd on the host via ssh
# assume the environment is already setup with an
# SSH_AUTH_SOCK that allows login
"""
import os
env = os.environ.copy()
if sess.socket is None:
raise SshAgentException("No ssh-agent yet")
env['SSH_AUTH_SOCK'] = sess.socket
exec_p = subprocess.Popen(['ssh', '-A', '-o', 'Stricthostkeychecking=no', '-l',
user, host, cmd],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
if stdin is not None:
(stdout, stderr) = exec_p.communicate(stdin.encode())
else:
(stdout, stderr) = exec_p.communicate()
return {'stdout':stdout, 'stderr':stderr}
@staticmethod
def tunnel(sess, port, batchhost, user, host, internalfirewall = True, localbind = True):
"""
the double tunnel is used if the server we run on the batch host is only
addressiable on localhost
e.g. jupyter in its default config runs on localhost:8888 so a web
browser on the login node can not connect to it
"""
print("localbind is",localbind,internalfirewall)
if port == 22:
print("port is 22, using single tunnel")
return Ssh.singletunnel(sess, port, batchhost, user, host)
if not internalfirewall and not localbind:
return Ssh.singletunnel(sess, port, batchhost, user, host)
print("using doubletunnel")
return Ssh.doubletunnel(sess, port, batchhost, user, host)
@staticmethod
def addkey(sess,key,cert):
pass
def singletunnel(sess, port, batchhost, user, host):
"""
# fork a daemon process to hold a tunnel open on a given port like
# ssh -l user -L tunnel host -N
"""
import os
env = os.environ.copy()
env['SSH_AUTH_SOCK'] = sess.socket
localport = Ssh.get_free_port()
tunnel_p = subprocess.Popen(['ssh', '-N', '-o', 'Stricthostkeychecking=no', '-L',
'{localport}:{batchhost}:{port}'.format(port=port,
localport=localport,
batchhost=batchhost),
'-l', user, host], stdin=subprocess.PIPE, env=env)
Ssh.wait_for_tunnel(localport)
sess.port = localport
sess.pids.append(tunnel_p.pid)
return localport, [tunnel_p.pid]
@staticmethod
def doubletunnel(sess, port, batchhost, user, host):
"""
# fork a daemon process to hold a tunnel open on a given port like
# ssh -l user -L tunnel host -N
"""
import os
env = os.environ.copy()
env['SSH_AUTH_SOCK'] = sess.socket
pids = []
localport1 = Ssh.get_free_port()
tunnel_p = subprocess.Popen(['ssh', '-N', '-o', 'Stricthostkeychecking=no', '-L',
'{localport1}:{batchhost}:22'.format(localport1=localport1,
batchhost=batchhost),
'-l', user, host], stdin=subprocess.PIPE, env=env)
pids.append(tunnel_p.pid)
Ssh.wait_for_tunnel(localport1)
localport2 = Ssh.get_free_port()
tunnel_p = subprocess.Popen(['ssh', '-N', '-o', 'Stricthostkeychecking=no', '-L',
'{localport2}:localhost:{port}'.format(port=port,
localport2=localport2),
'-l', user, 'localhost', '-p',
'{}'.format(localport1)], stdin=subprocess.PIPE, env=env)
pids.append(tunnel_p.pid)
Ssh.wait_for_tunnel(localport2)
sess.port = localport2
sess.pids.extend(pids)
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
return localport2, pids
@staticmethod
def wait_for_tunnel(localport):
"""
# In order to avoid a race condition, we wait for the tunnel to be established
"""
import socket
notopen = True
while notopen:
ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssock.setblocking(True)
try:
ssock.connect(('127.0.0.1', localport))
notopen = False
ssock.close()
except ConnectionRefusedError:
ssock.close()
return
@staticmethod
def get_free_port():
"""
# Finds a port which the local server can listen on.
"""
import socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
for testport in range(1025, 65500):
try:
serversocket.bind(('127.0.0.1', testport))
port = testport
serversocket.close()
return port
except OSError:
pass