ssh.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
  2. # Copyright: (c) <spug.dev@gmail.com>
  3. # Released under the AGPL-3.0 License.
  4. from paramiko.client import SSHClient, AutoAddPolicy
  5. from paramiko.rsakey import RSAKey
  6. from paramiko.ssh_exception import AuthenticationException
  7. from io import StringIO
  8. import base64
  9. import time
  10. import re
  11. class SSH:
  12. def __init__(self, hostname, port=22, username='root', pkey=None, password=None, default_env=None,
  13. connect_timeout=10):
  14. self.stdout = None
  15. self.client = None
  16. self.channel = None
  17. self.sftp = None
  18. self.eof = 'Spug EOF 2108111926'
  19. self.default_env = self._make_env_command(default_env)
  20. self.regex = re.compile(r'Spug EOF 2108111926 (-?\d+)[\r\n]?')
  21. self.arguments = {
  22. 'hostname': hostname,
  23. 'port': port,
  24. 'username': username,
  25. 'password': password,
  26. 'pkey': RSAKey.from_private_key(StringIO(pkey)) if isinstance(pkey, str) else pkey,
  27. 'timeout': connect_timeout,
  28. 'banner_timeout': 30
  29. }
  30. @staticmethod
  31. def generate_key():
  32. key_obj = StringIO()
  33. key = RSAKey.generate(2048)
  34. key.write_private_key(key_obj)
  35. return key_obj.getvalue(), 'ssh-rsa ' + key.get_base64()
  36. def get_client(self):
  37. if self.client is not None:
  38. return self.client
  39. self.client = SSHClient()
  40. self.client.set_missing_host_key_policy(AutoAddPolicy)
  41. self.client.connect(**self.arguments)
  42. return self.client
  43. def ping(self):
  44. return True
  45. def add_public_key(self, public_key):
  46. command = f'mkdir -p -m 700 ~/.ssh && \
  47. echo {public_key!r} >> ~/.ssh/authorized_keys && \
  48. chmod 600 ~/.ssh/authorized_keys'
  49. exit_code, out = self.exec_command_raw(command)
  50. if exit_code != 0:
  51. raise Exception(f'add public key error: {out}')
  52. def exec_command_raw(self, command, environment=None):
  53. channel = self.client.get_transport().open_session()
  54. if environment:
  55. channel.update_environment(environment)
  56. channel.set_combine_stderr(True)
  57. channel.exec_command(command)
  58. code, output = channel.recv_exit_status(), channel.recv(-1)
  59. try:
  60. output = output.decode()
  61. except UnicodeDecodeError:
  62. output = output.decode(encoding='GBK')
  63. return code, output
  64. def exec_command(self, command, environment=None):
  65. channel = self._get_channel()
  66. command = self._handle_command(command, environment)
  67. channel.send(command)
  68. out, exit_code = '', -1
  69. for line in self.stdout:
  70. match = self.regex.search(line)
  71. if match:
  72. exit_code = int(match.group(1))
  73. line = line[:match.start()]
  74. out += line
  75. break
  76. out += line
  77. return exit_code, out
  78. def _win_exec_command_with_stream(self, command, environment=None):
  79. channel = self.client.get_transport().open_session()
  80. if environment:
  81. channel.update_environment(environment)
  82. channel.set_combine_stderr(True)
  83. channel.get_pty(width=102)
  84. channel.exec_command(command)
  85. stdout = channel.makefile("rb", -1)
  86. out = stdout.readline()
  87. while out:
  88. yield channel.exit_status, out.decode()
  89. out = stdout.readline()
  90. yield channel.recv_exit_status(), out.decode()
  91. def exec_command_with_stream(self, command, environment=None):
  92. channel = self._get_channel()
  93. command = self._handle_command(command, environment)
  94. channel.send(command)
  95. exit_code, line = -1, ''
  96. while True:
  97. line = channel.recv(8196).decode()
  98. if not line:
  99. break
  100. match = self.regex.search(line)
  101. if match:
  102. exit_code = int(match.group(1))
  103. line = line[:match.start()]
  104. break
  105. yield exit_code, line
  106. yield exit_code, line
  107. def put_file(self, local_path, remote_path):
  108. sftp = self._get_sftp()
  109. sftp.put(local_path, remote_path)
  110. def put_file_by_fl(self, fl, remote_path, callback=None):
  111. sftp = self._get_sftp()
  112. sftp.putfo(fl, remote_path, callback=callback)
  113. def list_dir_attr(self, path):
  114. sftp = self._get_sftp()
  115. return sftp.listdir_attr(path)
  116. def remove_file(self, path):
  117. sftp = self._get_sftp()
  118. sftp.remove(path)
  119. def _get_channel(self):
  120. if self.channel:
  121. return self.channel
  122. counter = 0
  123. self.channel = self.client.invoke_shell()
  124. command = 'export PS1= && stty -echo; unsetopt zle; set -e\n'
  125. if self.default_env:
  126. command += f'{self.default_env}\n'
  127. command += f'echo {self.eof} $?\n'
  128. self.channel.send(command.encode())
  129. while True:
  130. if self.channel.recv_ready():
  131. line = self.channel.recv(8196).decode()
  132. if self.regex.search(line):
  133. self.stdout = self.channel.makefile('r')
  134. break
  135. elif counter >= 100:
  136. self.client.close()
  137. raise Exception('Wait spug response timeout')
  138. else:
  139. counter += 1
  140. time.sleep(0.1)
  141. return self.channel
  142. def _get_sftp(self):
  143. if self.sftp:
  144. return self.sftp
  145. self.sftp = self.client.open_sftp()
  146. return self.sftp
  147. def _break(self):
  148. time.sleep(5)
  149. command = f'\x03 echo {self.eof} -1\n'
  150. self.channel.send(command.encode())
  151. def _make_env_command(self, environment):
  152. if not environment:
  153. return None
  154. str_envs = []
  155. for k, v in environment.items():
  156. k = k.replace('-', '_')
  157. if isinstance(v, str):
  158. v = v.replace("'", "'\"'\"'")
  159. str_envs.append(f"{k}='{v}'")
  160. str_envs = ' '.join(str_envs)
  161. return f'export {str_envs}'
  162. def _handle_command(self, command, environment):
  163. new_command = f'trap \'echo {self.eof} $?; rm -f $SPUG_EXEC_FILE\' EXIT\n'
  164. env_command = self._make_env_command(environment)
  165. if env_command:
  166. new_command += f'{env_command}\n'
  167. new_command += command
  168. b64_command = base64.standard_b64encode(new_command.encode())
  169. commands = 'export SPUG_EXEC_FILE=$(mktemp)\n'
  170. commands += f'echo {b64_command.decode()} | base64 -di > $SPUG_EXEC_FILE\n'
  171. commands += 'bash $SPUG_EXEC_FILE\n'
  172. return commands
  173. def __enter__(self):
  174. self.get_client()
  175. transport = self.client.get_transport()
  176. if 'windows' in transport.remote_version.lower():
  177. self.exec_command = self.exec_command_raw
  178. self.exec_command_with_stream = self._win_exec_command_with_stream
  179. return self
  180. def __exit__(self, exc_type, exc_val, exc_tb):
  181. self.client.close()
  182. self.client = None