docker_remote_control.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  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. stdout, stderr, exit_code = self.execute_command(
  167. "docker ps -a --format '{{.Names}}\t{{.Status}}\t{{.Image}}'"
  168. )
  169. if exit_code == 0:
  170. containers = {}
  171. for line in stdout.strip().split('\n'):
  172. if line.strip():
  173. parts = line.split('\t')
  174. if len(parts) >= 2:
  175. name = parts[0]
  176. status = parts[1]
  177. image = parts[2] if len(parts) > 2 else ""
  178. containers[name] = {
  179. 'status': status,
  180. 'image': image
  181. }
  182. return containers
  183. else:
  184. print(f"✗ 获取容器状态失败: {stderr}")
  185. return {}
  186. def docker_logs(self, container_name: str, lines: int = 100, follow: bool = False) -> str:
  187. """查看容器日志"""
  188. cmd = f"docker logs --tail {lines} {container_name}"
  189. if follow:
  190. cmd += " -f"
  191. stdout, stderr, exit_code = self.execute_command(cmd)
  192. if exit_code == 0:
  193. return stdout
  194. else:
  195. return f"获取日志失败: {stderr}"
  196. # ========== 配置文件操作方法 ==========
  197. def read_config(self, config_file: str) -> Optional[Dict[str, Any]]:
  198. """读取远程配置文件"""
  199. remote_path = f"{self.project_path}/{config_file}"
  200. try:
  201. # 读取文件内容
  202. with self.sftp_client.open(remote_path, 'r') as f:
  203. content = f.read().decode('utf-8')
  204. return json.loads(content)
  205. except FileNotFoundError:
  206. print(f"✗ 配置文件不存在: {remote_path}")
  207. return None
  208. except json.JSONDecodeError as e:
  209. print(f"✗ JSON解析失败: {e}")
  210. return None
  211. except Exception as e:
  212. print(f"✗ 读取配置文件失败: {e}")
  213. return None
  214. def write_config(self, config_file: str, config_data: Dict[str, Any]) -> bool:
  215. """写入远程配置文件"""
  216. remote_path = f"{self.project_path}/{config_file}"
  217. try:
  218. # 将配置转换为JSON字符串
  219. json_str = json.dumps(config_data, indent=2, ensure_ascii=False)
  220. # 写入文件
  221. with self.sftp_client.open(remote_path, 'w') as f:
  222. f.write(json_str.encode('utf-8'))
  223. print(f"✓ 配置文件已更新: {config_file}")
  224. return True
  225. except Exception as e:
  226. print(f"✗ 写入配置文件失败: {e}")
  227. return False
  228. def update_config(self, config_file: str, key_path: str, value: Any) -> bool:
  229. """
  230. 更新配置文件中的指定键值
  231. Args:
  232. config_file: 配置文件路径(相对于项目根目录)
  233. key_path: 键路径,支持嵌套,如 "lockV1.sessionLimit" 或 "releaseTimePoint.hour"
  234. value: 新值
  235. """
  236. config = self.read_config(config_file)
  237. if config is None:
  238. return False
  239. # 解析键路径
  240. keys = key_path.split('.')
  241. current = config
  242. # 导航到目标位置
  243. for key in keys[:-1]:
  244. if key not in current:
  245. current[key] = {}
  246. current = current[key]
  247. # 设置值
  248. old_value = current.get(keys[-1])
  249. current[keys[-1]] = value
  250. print(f"配置更新: {key_path}")
  251. print(f" 旧值: {old_value}")
  252. print(f" 新值: {value}")
  253. return self.write_config(config_file, config)
  254. # ========== 日志文件查看方法 ==========
  255. def view_log(self, log_file: str, lines: int = 100, follow: bool = False) -> str:
  256. """
  257. 查看日志文件
  258. Args:
  259. log_file: 日志文件路径(相对于项目根目录或绝对路径)
  260. lines: 显示的行数
  261. follow: 是否实时跟踪(注意:follow模式在SSH中可能不稳定)
  262. """
  263. # 如果是相对路径,添加项目路径前缀
  264. if not log_file.startswith('/'):
  265. remote_path = f"{self.project_path}/{log_file}"
  266. else:
  267. remote_path = log_file
  268. if follow:
  269. # follow模式需要特殊处理
  270. cmd = f"tail -f {remote_path}"
  271. else:
  272. cmd = f"tail -n {lines} {remote_path}"
  273. stdout, stderr, exit_code = self.execute_command(cmd)
  274. if exit_code == 0:
  275. return stdout
  276. else:
  277. return f"查看日志失败: {stderr}"
  278. def read_log(
  279. self,
  280. log_file: str,
  281. lines: int = 100,
  282. from_head: bool = False,
  283. full: bool = False,
  284. output_path: Optional[str] = None,
  285. ) -> str:
  286. """
  287. 远程读取 logs 目录下指定的日志文件。
  288. 若 log_file 仅为文件名(如 slot_monitor.log),自动视为 logs/ 下文件。
  289. Args:
  290. log_file: 日志文件名或路径。纯文件名则自动加 logs/ 前缀。
  291. lines: 读取行数(默认 100)。full 时忽略。
  292. from_head: True 则读开头 N 行,否则读末尾 N 行。
  293. full: True 则读取整个文件。
  294. output_path: 若指定,将读取内容保存到本地该路径。
  295. Returns:
  296. 读取到的日志内容;失败时返回错误信息。
  297. """
  298. # 纯文件名(无路径)视为 logs 下
  299. if not log_file.startswith('/') and '/' not in log_file:
  300. remote_path = f"{self.project_path}/logs/{log_file}"
  301. elif not log_file.startswith('/'):
  302. remote_path = f"{self.project_path}/{log_file}"
  303. else:
  304. remote_path = log_file
  305. if full:
  306. cmd = f"cat {remote_path}"
  307. elif from_head:
  308. cmd = f"head -n {lines} {remote_path}"
  309. else:
  310. cmd = f"tail -n {lines} {remote_path}"
  311. stdout, stderr, exit_code = self.execute_command(cmd)
  312. if exit_code != 0:
  313. return f"读取日志失败: {stderr}"
  314. if output_path:
  315. try:
  316. path = Path(output_path)
  317. path.parent.mkdir(parents=True, exist_ok=True)
  318. path.write_text(stdout, encoding='utf-8')
  319. print(f"✓ 已保存到本地: {output_path}")
  320. except Exception as e:
  321. return f"保存到本地失败: {e}\n\n{stdout}"
  322. return stdout
  323. def list_logs(self) -> List[str]:
  324. """列出所有日志文件"""
  325. logs_dir = f"{self.project_path}/logs"
  326. stdout, stderr, exit_code = self.execute_command(
  327. f"ls -1 {logs_dir}/*.log 2>/dev/null || echo ''"
  328. )
  329. if exit_code == 0 and stdout.strip():
  330. log_files = [os.path.basename(f) for f in stdout.strip().split('\n') if f.strip()]
  331. return log_files
  332. return []
  333. def main():
  334. parser = argparse.ArgumentParser(
  335. description='Docker远程控制脚本',
  336. formatter_class=argparse.RawDescriptionHelpFormatter,
  337. epilog="""
  338. 使用示例:
  339. # 连接并查看容器状态
  340. python docker_remote_control.py --host 192.168.1.100 --password yourpass status
  341. # 重启容器
  342. python docker_remote_control.py --host 192.168.1.100 --password yourpass restart troov_monitor
  343. # 启动所有服务
  344. python docker_remote_control.py --host 192.168.1.100 --password yourpass up
  345. # 查看日志
  346. python docker_remote_control.py --host 192.168.1.100 --password yourpass logs troov_monitor --lines 50
  347. # 读取配置文件
  348. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-config config/troov_config.json
  349. # 更新配置文件
  350. python docker_remote_control.py --host 192.168.1.100 --password yourpass update-config config/troov_config.json lockV1.sessionLimit 10
  351. # 查看日志文件
  352. python docker_remote_control.py --host 192.168.1.100 --password yourpass view-log logs/slot_monitor.log --lines 100
  353. # 远程读取 logs 下指定日志(仅文件名自动加 logs/ 前缀)
  354. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log slot_monitor.log --lines 200
  355. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log slot_monitor.log --full --output ./downloaded.log
  356. python docker_remote_control.py --host 192.168.1.100 --password yourpass read-log session_creator.log --head --lines 50
  357. """
  358. )
  359. # 连接参数
  360. parser.add_argument('--host', required=True, help='远程服务器地址')
  361. parser.add_argument('--port', type=int, default=22, help='SSH端口 (默认: 22)')
  362. parser.add_argument('--username', default='root', help='SSH用户名 (默认: root)')
  363. parser.add_argument('--password', help='SSH密码')
  364. parser.add_argument('--key-file', help='SSH私钥文件路径')
  365. parser.add_argument('--project-path', default='/root/troov-asyncio',
  366. help='项目在远程服务器上的路径 (默认: /root/troov-asyncio)')
  367. # 子命令
  368. subparsers = parser.add_subparsers(dest='command', help='可用命令')
  369. # status命令
  370. subparsers.add_parser('status', help='查看所有容器状态')
  371. # docker控制命令
  372. up_parser = subparsers.add_parser('up', help='启动Docker Compose服务')
  373. up_parser.add_argument('services', nargs='*', help='要启动的服务名称(可选)')
  374. down_parser = subparsers.add_parser('down', help='停止Docker Compose服务')
  375. down_parser.add_argument('services', nargs='*', help='要停止的服务名称(可选)')
  376. restart_parser = subparsers.add_parser('restart', help='重启容器')
  377. restart_parser.add_argument('container', help='容器名称')
  378. start_parser = subparsers.add_parser('start', help='启动容器')
  379. start_parser.add_argument('container', help='容器名称')
  380. stop_parser = subparsers.add_parser('stop', help='停止容器')
  381. stop_parser.add_argument('container', help='容器名称')
  382. logs_parser = subparsers.add_parser('logs', help='查看容器日志')
  383. logs_parser.add_argument('container', help='容器名称')
  384. logs_parser.add_argument('--lines', type=int, default=100, help='显示行数 (默认: 100)')
  385. logs_parser.add_argument('--follow', action='store_true', help='实时跟踪日志')
  386. # 配置文件命令
  387. read_config_parser = subparsers.add_parser('read-config', help='读取配置文件')
  388. read_config_parser.add_argument('config_file', help='配置文件路径')
  389. update_config_parser = subparsers.add_parser('update-config', help='更新配置文件')
  390. update_config_parser.add_argument('config_file', help='配置文件路径')
  391. update_config_parser.add_argument('key_path', help='键路径,如 lockV1.sessionLimit')
  392. update_config_parser.add_argument('value', help='新值')
  393. # 日志文件命令
  394. view_log_parser = subparsers.add_parser('view-log', help='查看日志文件')
  395. view_log_parser.add_argument('log_file', help='日志文件路径')
  396. view_log_parser.add_argument('--lines', type=int, default=100, help='显示行数 (默认: 100)')
  397. view_log_parser.add_argument('--follow', action='store_true', help='实时跟踪日志')
  398. read_log_parser = subparsers.add_parser(
  399. 'read-log',
  400. help='远程读取 logs 下指定日志文件;仅文件名时自动加 logs/ 前缀',
  401. )
  402. read_log_parser.add_argument(
  403. 'log_file',
  404. help='日志文件名(如 slot_monitor.log)或路径(如 logs/slot_monitor.log)',
  405. )
  406. read_log_parser.add_argument('--lines', type=int, default=100, help='行数 (默认: 100)')
  407. read_log_parser.add_argument(
  408. '--head',
  409. action='store_true',
  410. help='读开头 N 行;默认读末尾 N 行',
  411. )
  412. read_log_parser.add_argument('--full', action='store_true', help='读取整个文件')
  413. read_log_parser.add_argument(
  414. '--output',
  415. dest='output_path',
  416. metavar='PATH',
  417. help='将读取内容保存到本地文件',
  418. )
  419. list_logs_parser = subparsers.add_parser('list-logs', help='列出所有日志文件')
  420. args = parser.parse_args()
  421. if not args.command:
  422. parser.print_help()
  423. sys.exit(1)
  424. # 检查认证方式
  425. if not args.password and not args.key_file:
  426. print("错误: 必须提供 --password 或 --key-file")
  427. sys.exit(1)
  428. # 创建控制器并连接
  429. controller = DockerRemoteController(
  430. host=args.host,
  431. port=args.port,
  432. username=args.username,
  433. password=args.password,
  434. key_file=args.key_file,
  435. project_path=args.project_path
  436. )
  437. if not controller.connect():
  438. sys.exit(1)
  439. try:
  440. # 执行命令
  441. if args.command == 'status':
  442. containers = controller.docker_status()
  443. print("\n容器状态:")
  444. print("-" * 80)
  445. for name, info in containers.items():
  446. print(f"{name:30} {info['status']:30} {info['image']}")
  447. elif args.command == 'up':
  448. controller.docker_compose_up(args.services if args.services else None)
  449. elif args.command == 'down':
  450. controller.docker_compose_down(args.services if args.services else None)
  451. elif args.command == 'restart':
  452. controller.docker_restart(args.container)
  453. elif args.command == 'start':
  454. controller.docker_start(args.container)
  455. elif args.command == 'stop':
  456. controller.docker_stop(args.container)
  457. elif args.command == 'logs':
  458. output = controller.docker_logs(args.container, args.lines, args.follow)
  459. print(output)
  460. elif args.command == 'read-config':
  461. config = controller.read_config(args.config_file)
  462. if config:
  463. print(json.dumps(config, indent=2, ensure_ascii=False))
  464. elif args.command == 'update-config':
  465. # 尝试将值转换为合适的类型
  466. value = args.value
  467. try:
  468. # 尝试转换为数字
  469. if value.isdigit() or (value.startswith('-') and value[1:].isdigit()):
  470. value = int(value)
  471. elif '.' in value and value.replace('.', '').replace('-', '').isdigit():
  472. value = float(value)
  473. # 尝试转换为布尔值
  474. elif value.lower() in ('true', 'false'):
  475. value = value.lower() == 'true'
  476. # 尝试转换为JSON
  477. elif value.startswith('{') or value.startswith('['):
  478. value = json.loads(value)
  479. except:
  480. pass # 保持为字符串
  481. controller.update_config(args.config_file, args.key_path, value)
  482. elif args.command == 'view-log':
  483. output = controller.view_log(args.log_file, args.lines, args.follow)
  484. print(output)
  485. elif args.command == 'read-log':
  486. output = controller.read_log(
  487. args.log_file,
  488. lines=args.lines,
  489. from_head=args.head,
  490. full=args.full,
  491. output_path=args.output_path,
  492. )
  493. print(output)
  494. elif args.command == 'list-logs':
  495. log_files = controller.list_logs()
  496. print("日志文件列表:")
  497. for log_file in log_files:
  498. print(f" - {log_file}")
  499. finally:
  500. controller.disconnect()
  501. if __name__ == '__main__':
  502. main()