Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

sshconn.py 7.7KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import paramiko
  2. import queue
  3. import urllib.parse
  4. import requests.adapters
  5. import logging
  6. import os
  7. import signal
  8. import socket
  9. import subprocess
  10. from docker.transport.basehttpadapter import BaseHTTPAdapter
  11. from .. import constants
  12. import urllib3
  13. import urllib3.connection
  14. RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
  15. class SSHSocket(socket.socket):
  16. def __init__(self, host):
  17. super().__init__(
  18. socket.AF_INET, socket.SOCK_STREAM)
  19. self.host = host
  20. self.port = None
  21. self.user = None
  22. if ':' in self.host:
  23. self.host, self.port = self.host.split(':')
  24. if '@' in self.host:
  25. self.user, self.host = self.host.split('@')
  26. self.proc = None
  27. def connect(self, **kwargs):
  28. args = ['ssh']
  29. if self.user:
  30. args = args + ['-l', self.user]
  31. if self.port:
  32. args = args + ['-p', self.port]
  33. args = args + ['--', self.host, 'docker system dial-stdio']
  34. preexec_func = None
  35. if not constants.IS_WINDOWS_PLATFORM:
  36. def f():
  37. signal.signal(signal.SIGINT, signal.SIG_IGN)
  38. preexec_func = f
  39. env = dict(os.environ)
  40. # drop LD_LIBRARY_PATH and SSL_CERT_FILE
  41. env.pop('LD_LIBRARY_PATH', None)
  42. env.pop('SSL_CERT_FILE', None)
  43. self.proc = subprocess.Popen(
  44. args,
  45. env=env,
  46. stdout=subprocess.PIPE,
  47. stdin=subprocess.PIPE,
  48. preexec_fn=preexec_func)
  49. def _write(self, data):
  50. if not self.proc or self.proc.stdin.closed:
  51. raise Exception('SSH subprocess not initiated.'
  52. 'connect() must be called first.')
  53. written = self.proc.stdin.write(data)
  54. self.proc.stdin.flush()
  55. return written
  56. def sendall(self, data):
  57. self._write(data)
  58. def send(self, data):
  59. return self._write(data)
  60. def recv(self, n):
  61. if not self.proc:
  62. raise Exception('SSH subprocess not initiated.'
  63. 'connect() must be called first.')
  64. return self.proc.stdout.read(n)
  65. def makefile(self, mode):
  66. if not self.proc:
  67. self.connect()
  68. self.proc.stdout.channel = self
  69. return self.proc.stdout
  70. def close(self):
  71. if not self.proc or self.proc.stdin.closed:
  72. return
  73. self.proc.stdin.write(b'\n\n')
  74. self.proc.stdin.flush()
  75. self.proc.terminate()
  76. class SSHConnection(urllib3.connection.HTTPConnection):
  77. def __init__(self, ssh_transport=None, timeout=60, host=None):
  78. super().__init__(
  79. 'localhost', timeout=timeout
  80. )
  81. self.ssh_transport = ssh_transport
  82. self.timeout = timeout
  83. self.ssh_host = host
  84. def connect(self):
  85. if self.ssh_transport:
  86. sock = self.ssh_transport.open_session()
  87. sock.settimeout(self.timeout)
  88. sock.exec_command('docker system dial-stdio')
  89. else:
  90. sock = SSHSocket(self.ssh_host)
  91. sock.settimeout(self.timeout)
  92. sock.connect()
  93. self.sock = sock
  94. class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
  95. scheme = 'ssh'
  96. def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None):
  97. super().__init__(
  98. 'localhost', timeout=timeout, maxsize=maxsize
  99. )
  100. self.ssh_transport = None
  101. self.timeout = timeout
  102. if ssh_client:
  103. self.ssh_transport = ssh_client.get_transport()
  104. self.ssh_host = host
  105. def _new_conn(self):
  106. return SSHConnection(self.ssh_transport, self.timeout, self.ssh_host)
  107. # When re-using connections, urllib3 calls fileno() on our
  108. # SSH channel instance, quickly overloading our fd limit. To avoid this,
  109. # we override _get_conn
  110. def _get_conn(self, timeout):
  111. conn = None
  112. try:
  113. conn = self.pool.get(block=self.block, timeout=timeout)
  114. except AttributeError: # self.pool is None
  115. raise urllib3.exceptions.ClosedPoolError(self, "Pool is closed.")
  116. except queue.Empty:
  117. if self.block:
  118. raise urllib3.exceptions.EmptyPoolError(
  119. self,
  120. "Pool reached maximum size and no more "
  121. "connections are allowed."
  122. )
  123. # Oh well, we'll create a new connection then
  124. return conn or self._new_conn()
  125. class SSHHTTPAdapter(BaseHTTPAdapter):
  126. __attrs__ = requests.adapters.HTTPAdapter.__attrs__ + [
  127. 'pools', 'timeout', 'ssh_client', 'ssh_params', 'max_pool_size'
  128. ]
  129. def __init__(self, base_url, timeout=60,
  130. pool_connections=constants.DEFAULT_NUM_POOLS,
  131. max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
  132. shell_out=False):
  133. self.ssh_client = None
  134. if not shell_out:
  135. self._create_paramiko_client(base_url)
  136. self._connect()
  137. self.ssh_host = base_url
  138. if base_url.startswith('ssh://'):
  139. self.ssh_host = base_url[len('ssh://'):]
  140. self.timeout = timeout
  141. self.max_pool_size = max_pool_size
  142. self.pools = RecentlyUsedContainer(
  143. pool_connections, dispose_func=lambda p: p.close()
  144. )
  145. super().__init__()
  146. def _create_paramiko_client(self, base_url):
  147. logging.getLogger("paramiko").setLevel(logging.WARNING)
  148. self.ssh_client = paramiko.SSHClient()
  149. base_url = urllib.parse.urlparse(base_url)
  150. self.ssh_params = {
  151. "hostname": base_url.hostname,
  152. "port": base_url.port,
  153. "username": base_url.username
  154. }
  155. ssh_config_file = os.path.expanduser("~/.ssh/config")
  156. if os.path.exists(ssh_config_file):
  157. conf = paramiko.SSHConfig()
  158. with open(ssh_config_file) as f:
  159. conf.parse(f)
  160. host_config = conf.lookup(base_url.hostname)
  161. if 'proxycommand' in host_config:
  162. self.ssh_params["sock"] = paramiko.ProxyCommand(
  163. host_config['proxycommand']
  164. )
  165. if 'hostname' in host_config:
  166. self.ssh_params['hostname'] = host_config['hostname']
  167. if base_url.port is None and 'port' in host_config:
  168. self.ssh_params['port'] = host_config['port']
  169. if base_url.username is None and 'user' in host_config:
  170. self.ssh_params['username'] = host_config['user']
  171. if 'identityfile' in host_config:
  172. self.ssh_params['key_filename'] = host_config['identityfile']
  173. self.ssh_client.load_system_host_keys()
  174. self.ssh_client.set_missing_host_key_policy(paramiko.RejectPolicy())
  175. def _connect(self):
  176. if self.ssh_client:
  177. self.ssh_client.connect(**self.ssh_params)
  178. def get_connection(self, url, proxies=None):
  179. if not self.ssh_client:
  180. return SSHConnectionPool(
  181. ssh_client=self.ssh_client,
  182. timeout=self.timeout,
  183. maxsize=self.max_pool_size,
  184. host=self.ssh_host
  185. )
  186. with self.pools.lock:
  187. pool = self.pools.get(url)
  188. if pool:
  189. return pool
  190. # Connection is closed try a reconnect
  191. if self.ssh_client and not self.ssh_client.get_transport():
  192. self._connect()
  193. pool = SSHConnectionPool(
  194. ssh_client=self.ssh_client,
  195. timeout=self.timeout,
  196. maxsize=self.max_pool_size,
  197. host=self.ssh_host
  198. )
  199. self.pools[url] = pool
  200. return pool
  201. def close(self):
  202. super().close()
  203. if self.ssh_client:
  204. self.ssh_client.close()