Python 远程连接服务器,用它就够了
想用一把趁手的“瑞士军刀”去连服务器、跑命令、传文件、开隧道?不绕弯,用 paramiko 就能把大多数日常需求打包搞定。下面这篇就是从零到一,给你一套能直接落地的方案:装库、跑通、避坑、封装一个小工具,最后再给出扩展方向。
选谁?为什么是 paramiko
paramiko 是纯 Python 的 SSHv2 实现,能做三件关键事:命令执行、SFTP 传输、端口转发/隧道。跨平台、稳定,依赖少;和系统 ssh 命令比,它在代码里可控程度更高(密钥、超时、跳板、重试都能自己掌握)。
安装就一行:
pip install paramiko
先跑通:最小可用示例
下面三段小代码,覆盖最常用的三件事:执行命令、上传、下载。
import paramikohost = "10.0.0.8"
user = "ubuntu"
pkey_path = "/home/me/.ssh/id_rsa"# 也可以用 password="..." 明文密码(不建议)
key = paramiko.RSAKey.from_private_key_file(pkey_path)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=host, username=user, pkey=key, timeout=10)
# 1) 执行命令
stdin, stdout, stderr = ssh.exec_command("uname -a && whoami", timeout=20)
print(stdout.read().decode())
print(stderr.read().decode())
# 2) 上传文件
sftp = ssh.open_sftp()
sftp.put("local.txt", "/tmp/remote.txt")
# 3) 下载文件
sftp.get("/var/log/syslog", "syslog.backup")
sftp.close()
ssh.close()
批量执行 & 并发
如果要在多台机器上跑同一条命令,用线程池就够了(IO 密集,线程更简单)。示例里带了超时、返回码、错误打印。
from concurrent.futures import ThreadPoolExecutor, as_completed
import paramikodefrun_cmd(host, user, key_path, cmd, timeout=20):
key = paramiko.RSAKey.from_private_key_file(key_path)
cli = paramiko.SSHClient(); cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
cli.connect(hostname=host, username=user, pkey=key, timeout=10)
_, stdout, stderr = cli.exec_command(cmd, timeout=timeout)
code = stdout.channel.recv_exit_status()
return host, code, stdout.read().decode(), stderr.read().decode()
except Exception as e:
return host, -1, "", str(e)
finally:
try: cli.close()
except: pass
hosts = ["10.0.0.8","10.0.0.9","10.0.0.10"]
with ThreadPoolExecutor(max_workers=8) as ex:
futs = [ex.submit(run_cmd, h, "ubuntu", "/home/me/.ssh/id_rsa", "uptime") for h in hosts]
for f in as_completed(futs):
h, code, out, err = f.result()
print(h, code, out.strip() or err.strip())
跳板机(堡垒机)/ ProxyJump
有时候只能先连跳板,再从跳板到内网机器。思路是:先连跳板,拿到一个 Channel,把它当成“套接字”给二跳 SSH 用(类似 ssh -J)。
import paramiko, socketdefconnect_via_bastion(bastion, target, user, key_path):
key = paramiko.RSAKey.from_private_key_file(key_path)
# 1) 先连跳板
bastion_cli = paramiko.SSHClient()
bastion_cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
bastion_cli.connect(bastion, username=user, pkey=key, timeout=10)
# 2) 通过跳板打开到目标主机的“直连通道”
transport = bastion_cli.get_transport()
dest_addr = (target, 22)
src_addr = ("127.0.0.1", 0)
channel = transport.open_channel("direct-tcpip", dest_addr, src_addr)
# 3) 用这个 channel 建立二跳 SSH
target_cli = paramiko.SSHClient()
target_cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
sock = channel # 把 channel 当 socket
target_cli.connect(target, username=user, pkey=key, sock=sock, timeout=10)
return bastion_cli, target_cli
bastion_cli, target_cli = connect_via_bastion("1.2.3.4", "10.0.0.8", "ubuntu", "/home/me/.ssh/id_rsa")
_, out, err = target_cli.exec_command("hostname && ip a")
print(out.read().decode() or err.read().decode())
target_cli.close(); bastion_cli.close()
本地端口转发(例如远程看数据库)
想把远端 10.0.0.8:5432 映射到本地 localhost:15432,这样本地客户端就能直接连接。paramiko 自带工具函数能写一个简单的 forwarder:
import select, socket, threading, paramikodefforward_tunnel(local_port, remote_host, remote_port, transport):
# 接收本地连接 -> 经由 SSH transport 转发到远端
server = socket.socket(); server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", local_port)); server.listen(100)
defhandler(client_sock):
try:
chan = transport.open_channel("direct-tcpip", (remote_host, remote_port), client_sock.getsockname())
except Exception:
client_sock.close(); return
whileTrue:
r, _, _ = select.select([client_sock, chan], [], [])
if client_sock in r:
data = client_sock.recv(1024)
ifnot data: break
chan.send(data)
if chan in r:
data = chan.recv(1024)
ifnot data: break
client_sock.send(data)
chan.close(); client_sock.close()
defloop():
whileTrue:
client, _ = server.accept()
threading.Thread(target=handler, args=(client,), daemon=True).start()
threading.Thread(target=loop, daemon=True).start()
ssh = paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("10.0.0.8", username="ubuntu", key_filename="/home/me/.ssh/id_rsa")
forward_tunnel(local_port=15432, remote_host="127.0.0.1", remote_port=5432, transport=ssh.get_transport())
input("本地已转发到 15432,按回车退出…\n")
长时间任务、sudo 与伪终端
exec_command 默认不是交互式终端。需要 sudo 的命令,常见做法是:要么免密 sudo,要么申请一个伪终端(PTY)并写入密码(生产环境更推荐免密+最小权限)。
import paramiko, timessh = paramiko.SSHClient(); ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("10.0.0.8", username="ubuntu", key_filename="/home/me/.ssh/id_rsa")
chan = ssh.invoke_shell() # 交互式 shell,带 pty
time.sleep(0.2); chan.recv(4096)
chan.send("sudo systemctl restart nginx\n")
time.sleep(0.2)
chan.send("your_password_here\n") # 更推荐免密 sudo,不在脚本里写密码
time.sleep(1)
print(chan.recv(65535).decode())
chan.close(); ssh.close()
跑长任务(比如备份、抓日志)可以用 nohup … & 或 tmux/screen,避免 SSH 掉线中断。
cmd = "nohup bash -c 'sleep 30; echo done >> /tmp/job.log' >/tmp/job.out 2>&1 &"
ssh.exec_command(cmd)
常见坑怎么躲
known_hosts:默认严格校验主机指纹。上面示例用了 AutoAddPolicy()直连方便开发,但生产建议自己维护known_hosts,或手工比对指纹。编码: stdout.read().decode(errors="ignore")可避免偶发编码错误。超时与重试: connect(timeout=...)、exec_command(timeout=...)都要设;网络抖动就重试。大输出:非常大的输出建议分块读,或把结果重定向到远端文件再 SFTP 回来。 权限:SFTP 上传目录权限不够是高频问题; chown/chmod常配套使用。
一个可复用的小工具(支持:命令/上传/下载/跳板/重试)
把上面的点揉成一个简单类,直接塞进你的项目即可。
import time, paramiko
from typing import Optional, TupleclassSSH:
def__init__(self, host: str, user: str, port: int = 22,
key_path: Optional[str] = None, password: Optional[str] = None,
bastion: Optional[str] = None, retries: int = 2, timeout: int = 10):
self.host, self.user, self.port = host, user, port
self.key_path, self.password = key_path, password
self.bastion, self.retries, self.timeout = bastion, retries, timeout
self._bcli = None# bastion client
self._cli = None# target client
def_connect_once(self):
pkey = None
if self.key_path:
try:
pkey = paramiko.RSAKey.from_private_key_file(self.key_path)
except paramiko.PasswordRequiredException:
# 如果你的私钥有口令,这里可调用 from_private_key_file(path, password=...)
raise
cli = paramiko.SSHClient(); cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
sock = None
if self.bastion:
bcli = paramiko.SSHClient(); bcli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
bcli.connect(self.bastion, username=self.user, pkey=pkey, password=self.password, timeout=self.timeout)
self._bcli = bcli
chan = bcli.get_transport().open_channel(
"direct-tcpip", (self.host, self.port), ("127.0.0.1", 0)
)
sock = chan
cli.connect(self.host, port=self.port, username=self.user,
pkey=pkey, password=self.password, sock=sock, timeout=self.timeout)
self._cli = cli
defconnect(self):
last = None
for i in range(self.retries + 1):
try:
self._connect_once(); return self
except Exception as e:
last = e; time.sleep(min(1 + i, 5))
raise last
defrun(self, cmd: str, timeout: Optional[int] = None) -> Tuple[int, str, str]:
ifnot self._cli: self.connect()
_, out, err = self._cli.exec_command(cmd, timeout=timeout or self.timeout)
code = out.channel.recv_exit_status()
return code, out.read().decode(errors="ignore"), err.read().decode(errors="ignore")
defupload(self, local: str, remote: str):
ifnot self._cli: self.connect()
sftp = self._cli.open_sftp()
try: sftp.put(local, remote)
finally: sftp.close()
defdownload(self, remote: str, local: str):
ifnot self._cli: self.connect()
sftp = self._cli.open_sftp()
try: sftp.get(remote, local)
finally: sftp.close()
defclose(self):
try:
if self._cli: self._cli.close()
finally:
try:
if self._bcli: self._bcli.close()
finally:
self._cli = self._bcli = None
# 用法演示
if __name__ == "__main__":
ssh = SSH(host="10.0.0.8", user="ubuntu", key_path="/home/me/.ssh/id_rsa",
bastion=None, retries=2, timeout=10).connect()
code, out, err = ssh.run("df -h && free -m")
print(code, out or err)
ssh.upload("app.tar.gz", "/tmp/app.tar.gz")
ssh.download("/var/log/syslog", "syslog.backup")
ssh.close()
何时考虑替代/补充
想要更“命令式”的语法: Fabric(基于paramiko,写起来更像脚本),适合成体系的部署流程。需要异步并发: asyncssh(asyncio风格),成百上千台并发时更轻盈。Windows 主机:用 pywinrm走 WinRM,比“在 Windows 上用 SSH 再 sudo”靠谱。
到这,一般日常“连上去把事儿办了”的诉求——无论单机、多机、跳板、传文件、开端口——一个 paramiko 就能打包覆盖。把上面的工具类丢进你的代码库,后面项目里基本就是三句话:connect()、run()、upload()/download()。剩下的,就是把超时、重试、权限这些小细节按你的环境稳稳拧紧。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领