docker_remote_control.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Docker远程控制脚本
  5. 功能:
  6. 1. 远程控制Docker容器的启动、停止、重启
  7. 2. 修改配置文件
  8. 3. 查看日志文件
  9. """
  10. import json
  11. import os
  12. import sys
  13. import argparse
  14. from pathlib import Path
  15. from typing import Optional, Dict, Any, List,Tuple
  16. import paramiko
  17. from io import StringIO
  18. class DockerRemoteController:
  19. """Docker远程控制器"""
  20. def __init__(self, host: str, port: int = 22, username: str = "root",
  21. password: Optional[str] = None, key_file: Optional[str] = None,
  22. project_path: str = "/root/troov-asyncio"):
  23. """
  24. 初始化远程控制器
  25. Args:
  26. host: 远程服务器地址
  27. port: SSH端口
  28. username: SSH用户名
  29. password: SSH密码
  30. key_file: SSH私钥文件路径
  31. project_path: 项目在远程服务器上的路径
  32. """
  33. self.host = host
  34. self.port = port
  35. self.username = username
  36. self.password = password
  37. self.key_file = key_file
  38. self.project_path = project_path
  39. self.ssh_client = None
  40. self.sftp_client = None
  41. def connect(self):
  42. """建立SSH连接"""
  43. try:
  44. self.ssh_client = paramiko.SSHClient()
  45. self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
  46. if self.key_file:
  47. # 使用密钥文件连接
  48. self.ssh_client.connect(
  49. hostname=self.host,
  50. port=self.port,
  51. username=self.username,
  52. key_filename=self.key_file
  53. )
  54. else:
  55. # 使用密码连接
  56. self.ssh_client.connect(
  57. hostname=self.host,
  58. port=self.port,
  59. username=self.username,
  60. password=self.password
  61. )
  62. # 创建SFTP客户端用于文件传输
  63. self.sftp_client = self.ssh_client.open_sftp()
  64. print(f"✓ 成功连接到 {self.host}:{self.port}")
  65. return True
  66. except Exception as e:
  67. print(f"✗ 连接失败: {e}")
  68. return False
  69. def disconnect(self):
  70. """关闭连接"""
  71. if self.sftp_client:
  72. self.sftp_client.close()
  73. if self.ssh_client:
  74. self.ssh_client.close()
  75. def execute_command(self, command: str) -> Tuple[str, str, int]:
  76. """
  77. 执行远程命令
  78. Returns:
  79. (stdout, stderr, exit_code)
  80. """
  81. if not self.ssh_client:
  82. raise Exception("未建立SSH连接,请先调用connect()")
  83. stdin, stdout, stderr = self.ssh_client.exec_command(command)
  84. exit_code = stdout.channel.recv_exit_status()
  85. stdout_text = stdout.read().decode('utf-8')
  86. stderr_text = stderr.read().decode('utf-8')
  87. return stdout_text, stderr_text, exit_code
  88. # ========== Docker控制方法 ==========
  89. def docker_compose_up(self, services: Optional[List[str]] = None) -> bool:
  90. """启动Docker Compose服务"""
  91. cmd = f"cd {self.project_path} && docker-compose up -d"
  92. if services:
  93. cmd += " " + " ".join(services)
  94. print(f"正在启动Docker服务...")
  95. stdout, stderr, exit_code = self.execute_command(cmd)
  96. if exit_code == 0:
  97. print("✓ Docker服务启动成功")
  98. if stdout:
  99. print(stdout)
  100. return True
  101. else:
  102. print(f"✗ Docker服务启动失败: {stderr}")
  103. return False
  104. def docker_compose_down(self, services: Optional[List[str]] = None) -> bool:
  105. """停止Docker Compose服务"""
  106. cmd = f"cd {self.project_path} && docker-compose down"
  107. if services:
  108. # docker-compose down不支持指定服务,需要单独停止
  109. for service in services:
  110. self.docker_stop(service)
  111. return True
  112. print(f"正在停止Docker服务...")
  113. stdout, stderr, exit_code = self.execute_command(cmd)
  114. if exit_code == 0:
  115. print("✓ Docker服务停止成功")
  116. if stdout:
  117. print(stdout)
  118. return True
  119. else:
  120. print(f"✗ Docker服务停止失败: {stderr}")
  121. return False
  122. def docker_restart(self, container_name: str) -> bool:
  123. """重启指定容器"""
  124. print(f"正在重启容器: {container_name}")
  125. stdout, stderr, exit_code = self.execute_command(
  126. f"docker restart {container_name}"
  127. )
  128. if exit_code == 0:
  129. print(f"✓ 容器 {container_name} 重启成功")
  130. if stdout:
  131. print(stdout)
  132. return True
  133. else:
  134. print(f"✗ 容器 {container_name} 重启失败: {stderr}")
  135. return False
  136. def docker_start(self, container_name: str) -> bool:
  137. """启动指定容器"""
  138. print(f"正在启动容器: {container_name}")
  139. stdout, stderr, exit_code = self.execute_command(
  140. f"docker start {container_name}"
  141. )
  142. if exit_code == 0:
  143. print(f"✓ 容器 {container_name} 启动成功")
  144. if stdout:
  145. print(stdout)
  146. return True
  147. else:
  148. print(f"✗ 容器 {container_name} 启动失败: {stderr}")
  149. return False
  150. def docker_stop(self, container_name: str) -> bool:
  151. """停止指定容器"""
  152. print(f"正在停止容器: {container_name}")
  153. stdout, stderr, exit_code = self.execute_command(
  154. f"docker stop {container_name}"
  155. )
  156. if exit_code == 0:
  157. print(f"✓ 容器 {container_name} 停止成功")
  158. if stdout:
  159. print(stdout)
  160. return True
  161. else:
  162. print(f"✗ 容器 {container_name} 停止失败: {stderr}")
  163. return False
  164. def docker_status(self) -> Dict[str, Any]:
  165. """查看指定项目路径下的容器状态"""
  166. # 1. 获取该项目下的所有容器 ID
  167. # 兼容 docker-compose v1 和 v2 (docker compose)
  168. id_cmd = f"cd {self.project_path} && (docker-compose ps -aq 2>/dev/null || docker compose ps -aq)"
  169. stdout_ids, stderr_ids, exit_code_ids = self.execute_command(id_cmd)
  170. container_ids = [cid.strip() for cid in stdout_ids.strip().split('\n') if cid.strip()]
  171. if not container_ids or exit_code_ids != 0:
  172. # 如果没找到容器或出错,返回空(或者可以保留原有逻辑作为回退,但用户要求只看对应的)
  173. if exit_code_ids != 0 and stderr_ids:
  174. print(f"✗ 获取项目容器列表失败: {stderr_ids}")
  175. return {}
  176. # 2. 根据 ID 获取这些容器的详细状态
  177. # 为了防止 ID 列表过长导致命令超长,我们可以分批或者使用过滤
  178. filters = " ".join([f"--filter id={cid}" for cid in container_ids])
  179. stdout, stderr, exit_code = self.execute_command(
  180. f"docker ps -a {filters} --format '{{{{.Names}}}}\t{{{{.Status}}}}\t{{{{.Image}}}}'"
  181. )
  182. if exit_code == 0:
  183. containers = {}
  184. for line in stdout.strip().split('\n'):
  185. if line.strip():
  186. parts = line.split('\t')
  187. if len(parts) >= 2:
  188. name = parts[0]
  189. status = parts[1]
  190. image = parts[2] if len(parts) > 2 else ""
  191. containers[name] = {
  192. 'status': status,
  193. 'image': image
  194. }
  195. return containers
  196. else:
  197. print(f"✗ 获取容器详细状态失败: {stderr}")
  198. return {}
  199. def docker_logs(self, container_name: str, lines: int = 100, follow: bool = False) -> str:
  200. """查看容器日志"""
  201. cmd = f"docker logs --tail {lines} {container_name}"
  202. if follow:
  203. cmd += " -f"
  204. stdout, stderr, exit_code = self.execute_command(cmd)
  205. if exit_code == 0:
  206. return stdout
  207. else:
  208. return f"获取日志失败: {stderr}"
  209. # ========== 配置文件操作方法 ==========
  210. def read_config(self, config_file: str) -> Optional[Dict[str, Any]]:
  211. """读取远程配置文件"""
  212. remote_path = f"{self.project_path}/{config_file}"
  213. try:
  214. # 读取文件内容
  215. with self.sftp_client.open(remote_path, 'r') as f:
  216. content = f.read().decode('utf-8')
  217. return json.loads(content)
  218. except FileNotFoundError:
  219. print(f"✗ 配置文件不存在: {remote_path}")
  220. return None
  221. except json.JSONDecodeError as e:
  222. print(f"✗ JSON解析失败: {e}")
  223. return None
  224. except Exception as e:
  225. print(f"✗ 读取配置文件失败: {e}")
  226. return None
  227. def write_config(self, config_file: str, config_data: Dict[str, Any]) -> bool:
  228. """写入远程配置文件"""
  229. remote_path = f"{self.project_path}/{config_file}"
  230. try:
  231. # 将配置转换为JSON字符串
  232. json_str = json.dumps(config_data, indent=2, ensure_ascii=False)
  233. # 写入文件
  234. with self.sftp_client.open(remote_path, 'w') as f:
  235. f.write(json_str.encode('utf-8'))
  236. print(f"✓ 配置文件已更新: {config_file}")
  237. return True
  238. except Exception as e:
  239. print(f"✗ 写入配置文件失败: {e}")
  240. return False
  241. def update_config(self, config_file: str, key_path: str, value: Any) -> bool:
  242. """
  243. 更新配置文件中的指定键值
  244. Args:
  245. config_file: 配置文件路径(相对于项目根目录)
  246. key_path: 键路径,支持嵌套,如 "lockV1.sessionLimit" 或 "releaseTimePoint.hour"
  247. value: 新值
  248. """
  249. config = self.read_config(config_file)
  250. if config is None:
  251. return False
  252. # 解析键路径
  253. keys = key_path.split('.')
  254. current = config
  255. # 导航到目标位置
  256. for key in keys[:-1]:
  257. if key not in current:
  258. current[key] = {}
  259. current = current[key]
  260. # 设置值
  261. old_value = current.get(keys[-1])
  262. current[keys[-1]] = value
  263. print(f"配置更新: {key_path}")
  264. print(f" 旧值: {old_value}")
  265. print(f" 新值: {value}")
  266. return self.write_config(config_file, config)
  267. # ========== 日志文件查看方法 ==========
  268. def view_log(self, log_file: str, lines: int = 100, follow: bool = False) -> str:
  269. """
  270. 查看日志文件
  271. Args:
  272. log_file: 日志文件路径(相对于项目根目录或绝对路径)
  273. lines: 显示的行数
  274. follow: 是否实时跟踪(注意:follow模式在SSH中可能不稳定)
  275. """
  276. # 如果是相对路径,添加项目路径前缀
  277. if not log_file.startswith('/'):
  278. remote_path = f"{self.project_path}/{log_file}"
  279. else:
  280. remote_path = log_file
  281. if follow:
  282. # follow模式需要特殊处理
  283. cmd = f"tail -f {remote_path}"
  284. else:
  285. cmd = f"tail -n {lines} {remote_path}"
  286. stdout, stderr, exit_code = self.execute_command(cmd)
  287. if exit_code == 0:
  288. return stdout
  289. else:
  290. return f"查看日志失败: {stderr}"
  291. def read_log(
  292. self,
  293. log_file: str,
  294. lines: int = 100,
  295. from_head: bool = False,
  296. full: bool = False,
  297. output_path: Optional[str] = None,
  298. ) -> str:
  299. """
  300. 远程读取 logs 目录下指定的日志文件。
  301. 若 log_file 仅为文件名(如 slot_monitor.log),自动视为 logs/ 下文件。
  302. Args:
  303. log_file: 日志文件名或路径。纯文件名则自动加 logs/ 前缀。
  304. lines: 读取行数(默认 100)。full 时忽略。
  305. from_head: True 则读开头 N 行,否则读末尾 N 行。
  306. full: True 则读取整个文件。
  307. output_path: 若指定,将读取内容保存到本地该路径。
  308. Returns:
  309. 读取到的日志内容;失败时返回错误信息。
  310. """
  311. # 纯文件名(无路径)视为 logs 下
  312. if not log_file.startswith('/') and '/' not in log_file:
  313. remote_path = f"{self.project_path}/logs/{log_file}"
  314. elif not log_file.startswith('/'):
  315. remote_path = f"{self.project_path}/{log_file}"
  316. else:
  317. remote_path = log_file
  318. if full:
  319. cmd = f"cat {remote_path}"
  320. elif from_head:
  321. cmd = f"head -n {lines} {remote_path}"
  322. else:
  323. cmd = f"tail -n {lines} {remote_path}"
  324. stdout, stderr, exit_code = self.execute_command(cmd)
  325. if exit_code != 0:
  326. return f"读取日志失败: {stderr}"
  327. if output_path:
  328. try:
  329. path = Path(output_path)
  330. path.parent.mkdir(parents=True, exist_ok=True)
  331. path.write_text(stdout, encoding='utf-8')
  332. print(f"✓ 已保存到本地: {output_path}")
  333. except Exception as e:
  334. return f"保存到本地失败: {e}\n\n{stdout}"
  335. return stdout
  336. def list_logs(self) -> List[str]:
  337. """列出所有日志文件"""
  338. logs_dir = f"{self.project_path}/logs"
  339. stdout, stderr, exit_code = self.execute_command(
  340. f"ls -1 {logs_dir}/*.log 2>/dev/null || echo ''"
  341. )
  342. if exit_code == 0 and stdout.strip():
  343. log_files = [os.path.basename(f) for f in stdout.strip().split('\n') if f.strip()]
  344. return log_files
  345. return []
  346. def main():
  347. parser = argparse.ArgumentParser(
  348. description='Docker远程控制脚本',
  349. formatter_class=argparse.RawDescriptionHelpFormatter,
  350. epilog="""
  351. 使用示例:
  352. # 连接并查看容器状态
  353. python docker_remote_control.py --host 192.168.1.100 --password yourpass status
  354. # 重启容器
  355. python docker_remote_control.py --host 192.168.1.100 --password yourpass restart troov_monitor
  356. # 启动所有服务
  357. python docker_remote_control.py --host 192.168.1.100 --password yourpass up
  358. # 查看日志
  359. python docker_remote_control.py --host 192.168.1.100 --password yourpass logs troov_monitor --lines 50
  360. # 读取配置文件
  361. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-config config/troov_config.json
  362. # 更新配置文件
  363. python docker_remote_control.py --host 192.168.1.100 --password yourpass update-config config/troov_config.json lockV1.sessionLimit 10
  364. # 查看日志文件
  365. python docker_remote_control.py --host 192.168.1.100 --password yourpass view-log logs/slot_monitor.log --lines 100
  366. # 远程读取 logs 下指定日志(仅文件名自动加 logs/ 前缀)
  367. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log slot_monitor.log --lines 200
  368. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log slot_monitor.log --full --output ./downloaded.log
  369. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log session_creator.log --head --lines 50
  370. """
  371. )
  372. # 连接参数
  373. parser.add_argument('--host', required=True, help='远程服务器地址')
  374. parser.add_argument('--port', type=int, default=22, help='SSH端口 (默认: 22)')
  375. parser.add_argument('--username', default='root', help='SSH用户名 (默认: root)')
  376. parser.add_argument('--password', help='SSH密码')
  377. parser.add_argument('--key-file', help='SSH私钥文件路径')
  378. parser.add_argument('--project-path', default='/root/troov-asyncio',
  379. help='项目在远程服务器上的路径 (默认: /root/troov-asyncio)')
  380. # 子命令
  381. subparsers = parser.add_subparsers(dest='command', help='可用命令')
  382. # status命令
  383. subparsers.add_parser('status', help='查看所有容器状态')
  384. # docker控制命令
  385. up_parser = subparsers.add_parser('up', help='启动Docker Compose服务')
  386. up_parser.add_argument('services', nargs='*', help='要启动的服务名称(可选)')
  387. down_parser = subparsers.add_parser('down', help='停止Docker Compose服务')
  388. down_parser.add_argument('services', nargs='*', help='要停止的服务名称(可选)')
  389. restart_parser = subparsers.add_parser('restart', help='重启容器')
  390. restart_parser.add_argument('container', help='容器名称')
  391. start_parser = subparsers.add_parser('start', help='启动容器')
  392. start_parser.add_argument('container', help='容器名称')
  393. stop_parser = subparsers.add_parser('stop', help='停止容器')
  394. stop_parser.add_argument('container', help='容器名称')
  395. logs_parser = subparsers.add_parser('logs', help='查看容器日志')
  396. logs_parser.add_argument('container', help='容器名称')
  397. logs_parser.add_argument('--lines', type=int, default=100, help='显示行数 (默认: 100)')
  398. logs_parser.add_argument('--follow', action='store_true', help='实时跟踪日志')
  399. # 配置文件命令
  400. read_config_parser = subparsers.add_parser('read-config', help='读取配置文件')
  401. read_config_parser.add_argument('config_file', help='配置文件路径')
  402. update_config_parser = subparsers.add_parser('update-config', help='更新配置文件')
  403. update_config_parser.add_argument('config_file', help='配置文件路径')
  404. update_config_parser.add_argument('key_path', help='键路径,如 lockV1.sessionLimit')
  405. update_config_parser.add_argument('value', help='新值')
  406. # 日志文件命令
  407. view_log_parser = subparsers.add_parser('view-log', help='查看日志文件')
  408. view_log_parser.add_argument('log_file', help='日志文件路径')
  409. view_log_parser.add_argument('--lines', type=int, default=100, help='显示行数 (默认: 100)')
  410. view_log_parser.add_argument('--follow', action='store_true', help='实时跟踪日志')
  411. read_log_parser = subparsers.add_parser(
  412. 'read-log',
  413. help='远程读取 logs 下指定日志文件;仅文件名时自动加 logs/ 前缀',
  414. )
  415. read_log_parser.add_argument(
  416. 'log_file',
  417. help='日志文件名(如 slot_monitor.log)或路径(如 logs/slot_monitor.log)',
  418. )
  419. read_log_parser.add_argument('--lines', type=int, default=100, help='行数 (默认: 100)')
  420. read_log_parser.add_argument(
  421. '--head',
  422. action='store_true',
  423. help='读开头 N 行;默认读末尾 N 行',
  424. )
  425. read_log_parser.add_argument('--full', action='store_true', help='读取整个文件')
  426. read_log_parser.add_argument(
  427. '--output',
  428. dest='output_path',
  429. metavar='PATH',
  430. help='将读取内容保存到本地文件',
  431. )
  432. list_logs_parser = subparsers.add_parser('list-logs', help='列出所有日志文件')
  433. args = parser.parse_args()
  434. if not args.command:
  435. parser.print_help()
  436. sys.exit(1)
  437. # 检查认证方式
  438. if not args.password and not args.key_file:
  439. print("错误: 必须提供 --password 或 --key-file")
  440. sys.exit(1)
  441. # 创建控制器并连接
  442. controller = DockerRemoteController(
  443. host=args.host,
  444. port=args.port,
  445. username=args.username,
  446. password=args.password,
  447. key_file=args.key_file,
  448. project_path=args.project_path
  449. )
  450. if not controller.connect():
  451. sys.exit(1)
  452. try:
  453. # 执行命令
  454. if args.command == 'status':
  455. containers = controller.docker_status()
  456. print("\n容器状态:")
  457. print("-" * 80)
  458. for name, info in containers.items():
  459. print(f"{name:30} {info['status']:30} {info['image']}")
  460. elif args.command == 'up':
  461. controller.docker_compose_up(args.services if args.services else None)
  462. elif args.command == 'down':
  463. controller.docker_compose_down(args.services if args.services else None)
  464. elif args.command == 'restart':
  465. controller.docker_restart(args.container)
  466. elif args.command == 'start':
  467. controller.docker_start(args.container)
  468. elif args.command == 'stop':
  469. controller.docker_stop(args.container)
  470. elif args.command == 'logs':
  471. output = controller.docker_logs(args.container, args.lines, args.follow)
  472. print(output)
  473. elif args.command == 'read-config':
  474. config = controller.read_config(args.config_file)
  475. if config:
  476. print(json.dumps(config, indent=2, ensure_ascii=False))
  477. elif args.command == 'update-config':
  478. # 尝试将值转换为合适的类型
  479. value = args.value
  480. try:
  481. # 尝试转换为数字
  482. if value.isdigit() or (value.startswith('-') and value[1:].isdigit()):
  483. value = int(value)
  484. elif '.' in value and value.replace('.', '').replace('-', '').isdigit():
  485. value = float(value)
  486. # 尝试转换为布尔值
  487. elif value.lower() in ('true', 'false'):
  488. value = value.lower() == 'true'
  489. # 尝试转换为JSON
  490. elif value.startswith('{') or value.startswith('['):
  491. value = json.loads(value)
  492. except:
  493. pass # 保持为字符串
  494. controller.update_config(args.config_file, args.key_path, value)
  495. elif args.command == 'view-log':
  496. output = controller.view_log(args.log_file, args.lines, args.follow)
  497. print(output)
  498. elif args.command == 'read-log':
  499. output = controller.read_log(
  500. args.log_file,
  501. lines=args.lines,
  502. from_head=args.head,
  503. full=args.full,
  504. output_path=args.output_path,
  505. )
  506. print(output)
  507. elif args.command == 'list-logs':
  508. log_files = controller.list_logs()
  509. print("日志文件列表:")
  510. for log_file in log_files:
  511. print(f" - {log_file}")
  512. finally:
  513. controller.disconnect()
  514. if __name__ == '__main__':
  515. main()