sms
parent
7360ac28bf
commit
d39d8dd0bb
|
|
@ -0,0 +1,464 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
safety-eval-service 一键发布到开发环境 K8s 脚本
|
||||||
|
================================================
|
||||||
|
在本地 Windows 执行,自动完成:
|
||||||
|
本地 Maven 编译 (可选) -> 上传 JAR -> 远程 Docker build -> 推送 ACR
|
||||||
|
-> 更新 K8s Deployment -> 滚动重启 -> 校验 Pod 状态
|
||||||
|
|
||||||
|
与 docs/build-tools/build_push.py 的区别:
|
||||||
|
build_push.py 只构建并推送镜像,最后只"打印" kubectl 命令;
|
||||||
|
本脚本会真正执行 K8s 部署、滚动更新并校验结果。
|
||||||
|
|
||||||
|
前置条件:
|
||||||
|
- Python 3 + paramiko (pip install paramiko)
|
||||||
|
- 本地已编译好 JAR,或加 --build 让脚本自动 mvn 编译
|
||||||
|
|
||||||
|
用法:
|
||||||
|
# 最简单: 自动查找项目 JAR 并发布 (推荐)
|
||||||
|
python push_to_dev.py
|
||||||
|
|
||||||
|
# 指定 JAR 路径
|
||||||
|
python push_to_dev.py --jar "D:\\workspace\\code\\safety-eval-service\\safety-eval-start\\target\\safety-eval-start-1.0-SNAPSHOT.jar"
|
||||||
|
|
||||||
|
# 先本地编译再发布
|
||||||
|
python push_to_dev.py --build
|
||||||
|
|
||||||
|
# 自定义版本序号 (默认自动递增当天序号)
|
||||||
|
python push_to_dev.py --version 3
|
||||||
|
|
||||||
|
# 只更新 K8s (镜像已存在,只触发滚动重启)
|
||||||
|
python push_to_dev.py --deploy-only --image <完整镜像地址>
|
||||||
|
|
||||||
|
# 跳过 K8s 部署,只构建推送镜像
|
||||||
|
python push_to_dev.py --no-deploy
|
||||||
|
|
||||||
|
示例输出镜像:
|
||||||
|
jjb-registry-registry.cn-hangzhou.cr.aliyuncs.com/ali_img_ns/prod-aly-ota-dragon-jjb-saas-safety-eval:ota-20260707-1
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
# ==================== 配置区 ====================
|
||||||
|
# K8s Master (SSH)
|
||||||
|
MASTER = "192.168.20.100"
|
||||||
|
SSH_USER = "root"
|
||||||
|
SSH_PASSWD = "Zcloud@zcloud100"
|
||||||
|
|
||||||
|
# 杭州 ACR (K8s image-pull-secret 指向此处,必须用这个仓库)
|
||||||
|
ACR_REGISTRY = "jjb-registry-registry.cn-hangzhou.cr.aliyuncs.com"
|
||||||
|
ACR_NAMESPACE = "ali_img_ns"
|
||||||
|
ACR_USER = "10952138@qq.com"
|
||||||
|
ACR_PASS = "idurCT!rIq9EzISD"
|
||||||
|
|
||||||
|
# K8s 部署信息
|
||||||
|
K8S_NAMESPACE = "jjb-dragon"
|
||||||
|
DEPLOY_NAME = "jjb-saas-safety-eval-deploy" # Deployment 名称
|
||||||
|
CONTAINER_NAME = "jjb-saas-safety-eval-app" # 容器名称
|
||||||
|
KUBECTL = "/usr/bin/kubectl"
|
||||||
|
|
||||||
|
# 镜像命名
|
||||||
|
APP_NAME = "jjb-saas-safety-eval"
|
||||||
|
ENV_PREFIX = "ota" # 环境前缀
|
||||||
|
|
||||||
|
# 项目本地路径 (默认为脚本所在目录的上一级)
|
||||||
|
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
# JAR 相对路径
|
||||||
|
JAR_REL = os.path.join("safety-eval-start", "target", "safety-eval-start-1.0-SNAPSHOT.jar")
|
||||||
|
|
||||||
|
# 远程构建目录
|
||||||
|
REMOTE_BUILD_DIR = f"/tmp/docker-build-{APP_NAME}"
|
||||||
|
|
||||||
|
# 脚本自带的 Dockerfile (与 docs/build-tools/Dockerfile 一致: CentOS7 + JDK1.8.0_202)
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DOCKERFILE_PATH = os.path.join(SCRIPT_DIR, "Dockerfile")
|
||||||
|
# 若 push 目录没有 Dockerfile,则回退到 docs/build-tools/Dockerfile
|
||||||
|
if not os.path.exists(DOCKERFILE_PATH):
|
||||||
|
DOCKERFILE_PATH = os.path.join(
|
||||||
|
PROJECT_DIR, "docs", "build-tools", "Dockerfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 工具函数 ====================
|
||||||
|
def ssh_exec(client, cmd, timeout=600, quiet=False):
|
||||||
|
"""执行 SSH 命令并打印结果,返回 (stdout, stderr, exit_code)。"""
|
||||||
|
if not quiet:
|
||||||
|
print(f" $ {cmd[:150]}{'...' if len(cmd) > 150 else ''}")
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout)
|
||||||
|
out = stdout.read().decode("utf-8", errors="replace")
|
||||||
|
err = stderr.read().decode("utf-8", errors="replace")
|
||||||
|
exit_code = stdout.channel.recv_exit_status()
|
||||||
|
if not quiet:
|
||||||
|
if out.strip():
|
||||||
|
for line in out.strip().split("\n")[-15:]:
|
||||||
|
print(f" {line}")
|
||||||
|
if err.strip():
|
||||||
|
# 过滤掉 docker 下载进度噪音
|
||||||
|
important = [
|
||||||
|
l for l in err.strip().split("\n")
|
||||||
|
if not any(x in l for x in
|
||||||
|
["Downloading", "Downloaded", "Extracting", "Progress",
|
||||||
|
"WARNING! --password", "Login Succeeded"])
|
||||||
|
]
|
||||||
|
if important:
|
||||||
|
print(f" [stderr] {'; '.join(important[:5])}")
|
||||||
|
return out.strip(), err.strip(), exit_code
|
||||||
|
|
||||||
|
|
||||||
|
def upload_with_progress(sftp, local, remote):
|
||||||
|
"""带进度条的上传。"""
|
||||||
|
size_mb = os.path.getsize(local) / 1024 / 1024
|
||||||
|
start = time.time()
|
||||||
|
last_pct = [-1]
|
||||||
|
|
||||||
|
def progress(transferred, total):
|
||||||
|
pct = int(transferred * 100 / total)
|
||||||
|
if pct >= last_pct[0] + 10:
|
||||||
|
last_pct[0] = pct
|
||||||
|
mb = transferred / 1024 / 1024
|
||||||
|
elapsed = time.time() - start
|
||||||
|
speed = mb / elapsed if elapsed > 0 else 0
|
||||||
|
print(f" {pct}% ({mb:.1f} MB) - {speed:.1f} MB/s")
|
||||||
|
|
||||||
|
sftp.put(local, remote, callback=progress)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
print(f" 上传完成: {elapsed:.1f}s ({size_mb / elapsed:.1f} MB/s)")
|
||||||
|
|
||||||
|
|
||||||
|
def find_local_jar(project_dir):
|
||||||
|
"""自动查找项目 JAR 包。"""
|
||||||
|
candidate = os.path.join(project_dir, JAR_REL)
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
return os.path.abspath(candidate)
|
||||||
|
# 兜底: 在 safety-eval-start/target 下找最大的 jar (非 *.original)
|
||||||
|
target_dir = os.path.join(project_dir, "safety-eval-start", "target")
|
||||||
|
if os.path.isdir(target_dir):
|
||||||
|
jars = [os.path.join(target_dir, f) for f in os.listdir(target_dir)
|
||||||
|
if f.endswith(".jar") and not f.endswith(".jar.original")]
|
||||||
|
if jars:
|
||||||
|
jars.sort(key=os.path.getsize, reverse=True)
|
||||||
|
return os.path.abspath(jars[0])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def local_mvn_build(project_dir):
|
||||||
|
"""在本地执行 Maven 编译。"""
|
||||||
|
print("[0/7] 本地 Maven 编译 (mvn clean package -DskipTests)...")
|
||||||
|
cmd = ["mvn", "clean", "package", "-DskipTests"]
|
||||||
|
print(f" $ {' '.join(cmd)}")
|
||||||
|
print(" (编译日志较多,仅显示关键输出...)")
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
cmd, cwd=project_dir, shell=(os.name == "nt"),
|
||||||
|
capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(" ERROR: 未找到 mvn 命令,请确认本地已安装 Maven 并加入 PATH。")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 打印最后的关键行
|
||||||
|
tail = (proc.stdout or "").strip().split("\n")[-25:]
|
||||||
|
for line in tail:
|
||||||
|
print(f" {line}")
|
||||||
|
if proc.returncode != 0:
|
||||||
|
print("\n ERROR: Maven 编译失败!")
|
||||||
|
if proc.stderr:
|
||||||
|
print(proc.stderr[-1000:])
|
||||||
|
sys.exit(1)
|
||||||
|
print(" Maven 编译成功。")
|
||||||
|
|
||||||
|
|
||||||
|
def next_version_suffix(client):
|
||||||
|
"""查询 master 上当天已存在的镜像 tag,自动递增序号。"""
|
||||||
|
date_tag = datetime.now().strftime("%Y%m%d")
|
||||||
|
prefix = f"{ENV_PREFIX}-{date_tag}-"
|
||||||
|
# 查询本应用当天已构建的 tag (docker images 第二列为 Tag)
|
||||||
|
repo_filter = f"prod-aly-{ENV_PREFIX}-dragon-{APP_NAME}"
|
||||||
|
out, _, _ = ssh_exec(
|
||||||
|
client,
|
||||||
|
f"docker images --format '{{{{.Repository}}}}:{{{{.Tag}}}}' "
|
||||||
|
f"| grep '{repo_filter}' | awk -F: '{{print $NF}}'",
|
||||||
|
quiet=True,
|
||||||
|
)
|
||||||
|
used = set()
|
||||||
|
for line in out.split("\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith(prefix):
|
||||||
|
num_part = line[len(prefix):]
|
||||||
|
if num_part.isdigit():
|
||||||
|
used.add(int(num_part))
|
||||||
|
return max(used) + 1 if used else 1
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 主流程 ====================
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="safety-eval-service 一键发布到开发环境 K8s",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=__doc__,
|
||||||
|
)
|
||||||
|
parser.add_argument("--jar", default=None, help="本地 JAR 路径 (不指定则自动查找)")
|
||||||
|
parser.add_argument("--build", action="store_true", help="发布前先执行本地 mvn 编译")
|
||||||
|
parser.add_argument("--version", default=None, help="镜像版本序号 (默认自动递增)")
|
||||||
|
parser.add_argument("--app", default=APP_NAME, help=f"应用名 (默认: {APP_NAME})")
|
||||||
|
parser.add_argument("--prefix", default=ENV_PREFIX, help=f"环境前缀 (默认: {ENV_PREFIX})")
|
||||||
|
parser.add_argument("--no-deploy", action="store_true", help="只构建推送镜像,不部署 K8s")
|
||||||
|
parser.add_argument("--deploy-only", action="store_true",
|
||||||
|
help="只更新 K8s (需配合 --image 指定已存在的镜像)")
|
||||||
|
parser.add_argument("--image", default=None, help="指定完整镜像地址 (配合 --deploy-only)")
|
||||||
|
parser.add_argument("--restart", action="store_true",
|
||||||
|
help="只重启 K8s (不改镜像,等同 rollout restart)")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="只打印计划,不实际执行")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
global APP_NAME, ENV_PREFIX
|
||||||
|
APP_NAME = args.app
|
||||||
|
ENV_PREFIX = args.prefix
|
||||||
|
|
||||||
|
# ---- 仅重启 ----
|
||||||
|
if args.restart:
|
||||||
|
do_k8s_restart(args.dry_run)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ---- 仅部署 (镜像已存在) ----
|
||||||
|
if args.deploy_only:
|
||||||
|
if not args.image:
|
||||||
|
print("ERROR: --deploy-only 需要配合 --image <完整镜像地址>")
|
||||||
|
sys.exit(1)
|
||||||
|
do_k8s_deploy(args.image, args.dry_run)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ==================== 完整流程: 编译+构建+推送+部署 ====================
|
||||||
|
date_tag = datetime.now().strftime("%Y%m%d")
|
||||||
|
|
||||||
|
# [0] 本地编译 (可选)
|
||||||
|
if args.build:
|
||||||
|
local_mvn_build(PROJECT_DIR)
|
||||||
|
|
||||||
|
# [1] 定位 JAR
|
||||||
|
jar_local = args.jar or find_local_jar(PROJECT_DIR)
|
||||||
|
if not jar_local or not os.path.exists(jar_local):
|
||||||
|
print(f"ERROR: 未找到 JAR 文件: {jar_local or '(未自动发现)'}")
|
||||||
|
print(f" 可用 --jar 指定路径,或加 --build 自动编译。")
|
||||||
|
sys.exit(1)
|
||||||
|
jar_size_mb = os.path.getsize(jar_local) / 1024 / 1024
|
||||||
|
jar_name = os.path.basename(jar_local)
|
||||||
|
|
||||||
|
# ---- 连接 master 以确定版本号 ----
|
||||||
|
print("\n连接 K8s Master 节点...")
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(MASTER, username=SSH_USER, password=SSH_PASSWD, timeout=30)
|
||||||
|
print(f" SSH 已连接: {MASTER}")
|
||||||
|
|
||||||
|
version_suffix = args.version or next_version_suffix(client)
|
||||||
|
image_tag = f"{ENV_PREFIX}-{date_tag}-{version_suffix}"
|
||||||
|
full_image = (f"{ACR_REGISTRY}/{ACR_NAMESPACE}/"
|
||||||
|
f"prod-aly-{ENV_PREFIX}-dragon-{APP_NAME}:{image_tag}")
|
||||||
|
|
||||||
|
# ---- 打印发布计划 ----
|
||||||
|
print("\n" + "=" * 66)
|
||||||
|
print(" safety-eval-service 一键发布到开发环境")
|
||||||
|
print("=" * 66)
|
||||||
|
print(f" JAR: {jar_local} ({jar_size_mb:.1f} MB)")
|
||||||
|
print(f" 应用: {APP_NAME}")
|
||||||
|
print(f" 镜像 Tag: {image_tag}")
|
||||||
|
print(f" 完整镜像: {full_image}")
|
||||||
|
print(f" K8s: {K8S_NAMESPACE}/{DEPLOY_NAME}")
|
||||||
|
print(f" Master: {MASTER}")
|
||||||
|
print("=" * 66)
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\n[DRY RUN] 以上为发布计划,未实际执行。")
|
||||||
|
client.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
# [2] 准备远程目录 + 上传 JAR
|
||||||
|
print(f"\n[1/6] 上传 JAR 到 master ({jar_size_mb:.1f} MB)...")
|
||||||
|
ssh_exec(client, f"rm -rf {REMOTE_BUILD_DIR} && mkdir -p {REMOTE_BUILD_DIR}/target",
|
||||||
|
quiet=True)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
remote_jar = f"{REMOTE_BUILD_DIR}/target/{jar_name}"
|
||||||
|
upload_with_progress(sftp, jar_local, remote_jar)
|
||||||
|
|
||||||
|
# 上传 Dockerfile
|
||||||
|
print(" 上传 Dockerfile...")
|
||||||
|
if os.path.exists(DOCKERFILE_PATH):
|
||||||
|
sftp.put(DOCKERFILE_PATH, f"{REMOTE_BUILD_DIR}/Dockerfile")
|
||||||
|
else:
|
||||||
|
# 回退: 用 heredoc 写入标准模板
|
||||||
|
print(" (未找到本地 Dockerfile,使用 master 上的模板)")
|
||||||
|
ssh_exec(client,
|
||||||
|
f"cp /opt/docker-templates/Dockerfile {REMOTE_BUILD_DIR}/Dockerfile "
|
||||||
|
f"2>/dev/null || true", quiet=True)
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
# [3] 准备构建上下文 (复制 JDK)
|
||||||
|
print("\n[2/6] 准备构建上下文 (复制 JDK)...")
|
||||||
|
ssh_exec(client, f"cp -r /opt/jdk1.8.0_202 {REMOTE_BUILD_DIR}/", timeout=120)
|
||||||
|
ssh_exec(client, f"du -sh {REMOTE_BUILD_DIR}/")
|
||||||
|
|
||||||
|
# [4] Docker build
|
||||||
|
print("\n[3/6] 构建 Docker 镜像...")
|
||||||
|
out, err, code = ssh_exec(
|
||||||
|
client, f"docker build -t '{full_image}' {REMOTE_BUILD_DIR}", timeout=600
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
print("\nERROR: Docker build 失败!")
|
||||||
|
client.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# [5] 登录 + 推送 ACR
|
||||||
|
print("\n[4/6] 推送镜像到杭州 ACR...")
|
||||||
|
ssh_exec(client,
|
||||||
|
f"echo '{ACR_PASS}' | docker login --username={ACR_USER} "
|
||||||
|
f"--password-stdin {ACR_REGISTRY}", quiet=True)
|
||||||
|
out, err, code = ssh_exec(client, f"docker push '{full_image}'", timeout=900)
|
||||||
|
if code != 0:
|
||||||
|
print("\nERROR: Docker push 失败!")
|
||||||
|
client.close()
|
||||||
|
sys.exit(1)
|
||||||
|
ssh_exec(client, f"docker images | grep '{APP_NAME}' | head -5")
|
||||||
|
|
||||||
|
# 清理远程构建目录
|
||||||
|
ssh_exec(client, f"rm -rf {REMOTE_BUILD_DIR}", quiet=True)
|
||||||
|
|
||||||
|
if args.no_deploy:
|
||||||
|
client.close()
|
||||||
|
print("\n" + "=" * 66)
|
||||||
|
print(" 构建推送完成 (--no-deploy 已跳过 K8s 部署)")
|
||||||
|
print("=" * 66)
|
||||||
|
print(f"\n 镜像地址: {full_image}\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
# [6] 部署到 K8s
|
||||||
|
print("\n[5/6] 部署到 K8s...")
|
||||||
|
do_k8s_deploy(full_image, dry_run=False, client=client)
|
||||||
|
|
||||||
|
# [7] 校验
|
||||||
|
print("\n[6/6] 校验 Pod 状态...")
|
||||||
|
verify_rollout(client)
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
print("\n" + "=" * 66)
|
||||||
|
print(" 发布完成!")
|
||||||
|
print("=" * 66)
|
||||||
|
print(f" 镜像: {full_image}")
|
||||||
|
print(f" 服务: safety-eval-service (Nacos: {K8S_NAMESPACE})")
|
||||||
|
print(f" 网关: http://192.168.20.100:30140/safety-eval/ (Header: filterGatewaName: cqanquan)")
|
||||||
|
print("=" * 66)
|
||||||
|
|
||||||
|
|
||||||
|
def do_k8s_deploy(full_image, dry_run, client=None):
|
||||||
|
"""更新 K8s Deployment 镜像并触发滚动更新。"""
|
||||||
|
own_client = client is None
|
||||||
|
if own_client:
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(MASTER, username=SSH_USER, password=SSH_PASSWD, timeout=30)
|
||||||
|
|
||||||
|
# 检查 Deployment 是否存在
|
||||||
|
out, _, code = ssh_exec(
|
||||||
|
client,
|
||||||
|
f"{KUBECTL} get deployment {DEPLOY_NAME} -n {K8S_NAMESPACE} --ignore-not-found",
|
||||||
|
quiet=True,
|
||||||
|
)
|
||||||
|
deploy_exists = code == 0 and DEPLOY_NAME in out
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print(f" [DRY RUN] 将更新镜像: {full_image}")
|
||||||
|
if own_client:
|
||||||
|
client.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
if deploy_exists:
|
||||||
|
# 更新镜像
|
||||||
|
print(f" 更新 Deployment 镜像...")
|
||||||
|
out, err, code = ssh_exec(
|
||||||
|
client,
|
||||||
|
f"{KUBECTL} set image deployment/{DEPLOY_NAME} "
|
||||||
|
f"{CONTAINER_NAME}={full_image} -n {K8S_NAMESPACE}"
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
print(f" ERROR: kubectl set image 失败: {err}")
|
||||||
|
if own_client:
|
||||||
|
client.close()
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# 首次部署: 用 docs/deploy 下的 yaml 创建
|
||||||
|
print(f" Deployment 不存在,尝试 apply 部署 yaml...")
|
||||||
|
deploy_yaml = f"/tmp/{DEPLOY_NAME}.yaml"
|
||||||
|
local_yaml = os.path.join(PROJECT_DIR, "docs", "deploy", "safety-eval-deploy.yaml")
|
||||||
|
if os.path.exists(local_yaml):
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
sftp.put(local_yaml, deploy_yaml)
|
||||||
|
sftp.close()
|
||||||
|
# 先替换镜像为本次构建的镜像
|
||||||
|
ssh_exec(client,
|
||||||
|
f"sed -i 's|image: .*|image: {full_image}|' {deploy_yaml}",
|
||||||
|
quiet=True)
|
||||||
|
ssh_exec(client, f"{KUBECTL} apply -f {deploy_yaml} -n {K8S_NAMESPACE}")
|
||||||
|
ssh_exec(client, f"rm -f {deploy_yaml}", quiet=True)
|
||||||
|
else:
|
||||||
|
print(" ERROR: Deployment 不存在且找不到部署 yaml,请先手动创建 Deployment。")
|
||||||
|
if own_client:
|
||||||
|
client.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if own_client:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def do_k8s_restart(dry_run):
|
||||||
|
"""仅重启 K8s (不改镜像)。"""
|
||||||
|
print("=" * 66)
|
||||||
|
print(f" 重启 K8s Deployment: {K8S_NAMESPACE}/{DEPLOY_NAME}")
|
||||||
|
print("=" * 66)
|
||||||
|
if dry_run:
|
||||||
|
print(" [DRY RUN] 将执行 rollout restart")
|
||||||
|
return
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(MASTER, username=SSH_USER, password=SSH_PASSWD, timeout=30)
|
||||||
|
print("\n触发滚动重启...")
|
||||||
|
ssh_exec(client, f"{KUBECTL} rollout restart deployment/{DEPLOY_NAME} -n {K8S_NAMESPACE}")
|
||||||
|
verify_rollout(client)
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_rollout(client):
|
||||||
|
"""等待滚动更新完成并打印 Pod 状态与最近日志。"""
|
||||||
|
print(" 等待滚动更新完成 (最多 300s)...")
|
||||||
|
out, err, code = ssh_exec(
|
||||||
|
client,
|
||||||
|
f"{KUBECTL} rollout status deployment/{DEPLOY_NAME} "
|
||||||
|
f"-n {K8S_NAMESPACE} --timeout=300s",
|
||||||
|
timeout=320,
|
||||||
|
)
|
||||||
|
if code != 0:
|
||||||
|
print(f" ⚠ 滚动更新未完成或超时: {err}")
|
||||||
|
print(" 最近事件:")
|
||||||
|
ssh_exec(client,
|
||||||
|
f"{KUBECTL} get events -n {K8S_NAMESPACE} --sort-by=.metadata.creationTimestamp "
|
||||||
|
f"| grep -i 'safety-eval' | tail -15")
|
||||||
|
|
||||||
|
# Pod 状态
|
||||||
|
print("\n Pod 状态:")
|
||||||
|
ssh_exec(client, f"{KUBECTL} get pods -n {K8S_NAMESPACE} -o wide | grep 'safety-eval' | head -10")
|
||||||
|
|
||||||
|
# 最近日志
|
||||||
|
print("\n 最近启动日志 (最后 40 行):")
|
||||||
|
ssh_exec(client,
|
||||||
|
f"{KUBECTL} logs deployment/{DEPLOY_NAME} -n {K8S_NAMESPACE} --tail=40")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue