用 Python 远程控制 Windows 服务器,太好用了!
我先说个场景啊,你肯定有共鸣的那种。
那天晚上快十一点多了,我在公司楼下啃着冷掉的炸鸡,运维那边微信狂响: “东哥,那个 Windows 服务器又得重启一下,顺便发个补丁脚本,能帮忙远程搞下不?”
我心里当时就只有一个念头——我要用 Python 把这些破事全自动化了,再也不想登远程桌面点来点去了,你们是不是也有这种时刻 😂
下面我就边聊边给你把这套东西理一遍,都是我自己踩坑之后沉淀出来的,代码用 Python,尽量说人话,你照着抄基本就能跑起来。
先说最实用的那一个:WinRM + pywinrm
Windows 自带一个远程管理通道叫 WinRM,你可以理解成“给 PowerShell 开了个远程入口”,Python 这边就用 pywinrm 去连它,特别顺手。
1)Windows 服务器上先开个门
在目标 Windows 上(管理员 PowerShell)执行几行:
# 开启远程管理
Enable-PSRemoting -Force# 如果只在内网用,可以先允许 HTTP(5985端口)
winrm quickconfig
# 可选:放开防火墙(按需来)
# netsh advfirewall firewall add rule name="WinRM 5985" dir=in action=allow protocol=TCP localport=5985
如果你们安全要求比较高,建议配 HTTPS(5986)和证书,但那一套稍微麻烦点,后面你习惯了再折腾。
2)Python 端用 pywinrm 连过去
先安装依赖:
pip install pywinrm
然后最简单的远程执行命令就这样:
import winrmsession = winrm.Protocol(
endpoint='http://192.168.1.100:5985/wsman',
transport='ntlm',
username=r'DOMAIN\\admin_user',
password='Str0ng_Passw0rd!',
)
shell_id = session.open_shell()
command_id = session.run_command(shell_id, 'ipconfig', ['/all'])
stdout, stderr, rc = session.get_command_output(shell_id, command_id)
print('退出码:', rc)
print('标准输出:\n', stdout.decode('gbk', errors='ignore'))
print('错误输出:\n', stderr.decode('gbk', errors='ignore'))
session.close_shell(shell_id)
几个小点你记一下就好:
endpoint就是那台 Windows 的地址+端口域账户记得 DOMAIN\\user这种写法中文一般是 gbk,不然会乱码
顺手说一句,我之前写数据库那篇文章的时候也吐槽过默认配置这事儿,很多“默认值”在生产上都挺坑人,WinRM 这玩意儿默认也挺保守的,超时、并发数都可以根据场景调一调。
我一般会封装个小工具类,不然每次写 session 太啰嗦
随手给你丢个能直接用的版本,你可以根据自己需要改改:
import winrm
from typing import List, Tuple, OptionalclassWindowsRemote:
def__init__(
self,
host: str,
username: str,
password: str,
port: int = 5985,
use_https: bool = False,
domain: Optional[str] = None,
):
self.host = host
self.port = port
self.use_https = use_https
if domain and'\\'notin username:
username = rf"{domain}\\{username}"
scheme = 'https'if use_https else'http'
endpoint = f'{scheme}://{host}:{port}/wsman'
self._protocol = winrm.Protocol(
endpoint=endpoint,
transport='ntlm',
username=username,
password=password,
server_cert_validation='ignore'if use_https else'ignore',
)
self._shell_id = self._protocol.open_shell()
defrun_cmd(self, command: str, args: Optional[List[str]] = None) -> Tuple[int, str, str]:
args = args or []
cmd_id = self._protocol.run_command(self._shell_id, command, args)
stdout, stderr, rc = self._protocol.get_command_output(self._shell_id, cmd_id)
# 粗暴处理下编码问题
out = stdout.decode('gbk', errors='ignore') or stdout.decode('utf-8', errors='ignore')
err = stderr.decode('gbk', errors='ignore') or stderr.decode('utf-8', errors='ignore')
return rc, out, err
defrun_ps(self, script: str) -> Tuple[int, str, str]:
cmd_id = self._protocol.run_command(self._shell_id, 'powershell', ['-NoProfile', '-NonInteractive', '-'])
self._protocol.send_input(self._shell_id, cmd_id, script.encode('utf-8'))
stdout, stderr, rc = self._protocol.get_command_output(self._shell_id, cmd_id)
out = stdout.decode('utf-8', errors='ignore')
err = stderr.decode('utf-8', errors='ignore')
return rc, out, err
defclose(self):
if self._shell_id:
self._protocol.close_shell(self._shell_id)
self._shell_id = None
def__del__(self):
try:
self.close()
except Exception:
pass
用的时候就很舒服:
if __name__ == '__main__':
client = WindowsRemote(
host='192.168.1.100',
username='admin',
password='YourPassword123',
domain='MYDOMAIN', # 没域就传 None
)# 1. 来个最简单的
rc, out, err = client.run_cmd('hostname')
print('hostname =>', out.strip())
# 2. 跑一段 PowerShell
ps = r"""
Get-Service | Where-Object {$_.Status -eq 'Running'} | Select-Object -First 5 | Format-Table -AutoSize
"""
rc, out, err = client.run_ps(ps)
print(out)
client.close()
有了这个类,什么重启服务、拉日志、执行脚本,就都变成一行一行的 Python 调用,爽得很。
那文件怎么弄?上传下载总得有吧
纯 pywinrm 其实是没有现成“传文件接口”的,但你绕一下就好:用 PowerShell 把文件内容做 Base64 发过去再写回磁盘。
Python 这边这么搞:
import base64
from pathlib import Pathdefupload_file(client: WindowsRemote, local_path: str, remote_path: str):
content = Path(local_path).read_bytes()
b64 = base64.b64encode(content).decode('ascii')
ps = f"""
$bytes = [System.Convert]::FromBase64String("{b64}");
[System.IO.File]::WriteAllBytes("{remote_path}", $bytes);
"""
rc, out, err = client.run_ps(ps)
if rc != 0:
raise RuntimeError(f'上传失败: {err}')
下载反过来:
defdownload_file(client: WindowsRemote, remote_path: str, local_path: str):
ps = f"""
$bytes = [System.IO.File]::ReadAllBytes("{remote_path}");
$b64 = [System.Convert]::ToBase64String($bytes);
Write-Output $b64;
"""
rc, out, err = client.run_ps(ps)
if rc != 0:
raise RuntimeError(f'下载失败: {err}') data = base64.b64decode(out.strip())
Path(local_path).write_bytes(data)
这个方案的好处就是:只要能执行 PowerShell,你就能传文件,不用额外开放 SMB 共享端口,对安全、运维都友好一点。
顺口聊聊:你可能并不只有 WinRM 这一条路
有的人问我,“东哥,我们机器上已经开了 SSH,要不直接用 paramiko 得了?”也行,其实 Windows 10 / 2019 以后可以装 OpenSSH Server,你如果习惯 Linux 那套,其实这更顺手。
大概是这样:
# Windows Powershell 安装 OpenSSH Server(有的版本已经自带)
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'
Python 这边:
import paramikossh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
hostname='192.168.1.100',
port=22,
username='Administrator',
password='YourPassword123',
)
stdin, stdout, stderr = ssh.exec_command('whoami')
print(stdout.read().decode())
# 传文件
sftp = ssh.open_sftp()
sftp.put('local.txt', r'C:\\temp\\remote.txt')
sftp.close()
ssh.close()
SSH 这套的优势在于:生态成熟,很多公司安全规范里本来就要求统一走 SSH,这时候你就可以把 Windows 当成“有点别扭的 Linux”来用。
不过呢,跟我之前吐槽 SpringBoot 默认配置很坑一样,SSH 默认的一些超时、连接数,也要自己调调,不然一上生产,线程池和连接池那堆问题照样会找你麻烦。
再说一个稍微“偏底层”的:WMI / pywin32 控控制服务和进程
如果你需要的是那种:查某个服务状态、杀进程、看 CPU/内存,WMI 用起来还挺顺滑的,就是 API 命名有点怪。
安装:
pip install wmi pywin32
然后:
import wmiconn = wmi.WMI(
computer="192.168.1.100",
user=r"MYDOMAIN\\admin",
password="YourPassword123"
)
# 查询正在运行的进程前几条
for process in conn.Win32_Process()[:5]:
print(process.ProcessId, process.Name)
# 操作服务
for svc in conn.Win32_Service(Name="Spooler"):
print('Print Spooler 状态:', svc.State)
# 重启服务
result, = svc.StopService()
print('Stop result:', result)
WMI 最大的优点就是对 Windows 世界非常“本地化”:什么事件日志、系统信息、硬件状态,全都有对应的类。但缺点也明显:API 又长又丑,文档看起来头皮发麻,有点像你在抓 TCP 包时看偏移量那种感觉……
安全这块你一定要上点心,不然真的是自己给自己挖坑
我见过好几次事故都是这种: 代码里把 Windows 管理员密码明文写死,后来项目 Git 仓库一泄露,整个内网的服务器跟自助餐一样…
做几件小事就能好很多:
不要把密码写死在代码里 用环境变量或配置文件 + 加密,例如:
import osPASSWORD = os.getenv('WIN_ADMIN_PWD')
给远程账户最小权限 不要上来就用 Domain Admin,能单机本地管理员就先单机,能只管某些服务就只给那块权限。
日志里不要乱打敏感信息 比如你只记命令和返回码,不要把完整连接字符串、密码、Token 打出来。
超时、重试、并发要控一下 不然脚本一写不好,跟个 DDoS 工具似的狂怼你自己的服务器…
最后我一般会把这些东西糅到一个“小运维脚本”里
比如给产品环境的 Windows 服务器做这些动作:
批量拉最新发布包 关服务 → 发包 → 清缓存 → 开服务 验证进程和端口是不是正常起来了 不对劲就自动回滚
思路其实就三步:
用一个 servers.yaml维护所有机器和角色(web、job、file 等)用上面那个 WindowsRemote类做所有远程执行 & 传文件给每个“动作”写成一个 Python 函数,按顺序调
你搞一遍之后,以后别人跟你说“东哥,帮我重启一下那几台 Windows”,你就可以一边喝奶茶一边敲一句:
python deploy_windows.py --env=prod --action=restart-iis
人还坐在工位上,服务器已经在机房里乖乖听话了,这种感觉…你懂的 😏
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB