21 Commits

Author SHA1 Message Date
sundapeng.sdp
b6d49ea562 feat: 优化Argus-Metric测试流程README.md;
refs #3
2025-10-11 16:50:28 +08:00
sundapeng.sdp
5211769ba8 feat: 测试环境搭建,e2e测试;
refs #3
2025-10-11 15:52:50 +08:00
sundapeng.sdp
28ef5df6e4 feat: 动态监测各组件健康状态,组件进程失败后动态拉起;
refs #11
2025-10-11 15:52:50 +08:00
sundapeng.sdp
687d29993a feat: 创建Grafana Dashboard基础配置;各镜像构建新增dns-monitor.sh脚本及相关定时任务; 2025-10-11 15:52:50 +08:00
sundapeng.sdp
54185101f0 feat: Dashboard支持 instance、hostname 不同var的过滤数据;
refs #12
2025-10-11 15:52:50 +08:00
sundapeng.sdp
018cdb40cc fix: 修复组件统一安装时,各组件入参不匹配的问题;
refs #11
2025-10-11 15:52:49 +08:00
sundapeng.sdp
36290bb08f feat: 基于原镜像构建对应的版本,解决账户兼容与持久化问题;创建 Dashboard 模版,default_dashboard.json;
refs #12
2025-10-11 15:52:49 +08:00
sundapeng.sdp
d514283cda fix: 修复 exporter_config.json 拷贝失败的问题;
refs #9
2025-10-11 15:52:49 +08:00
sundapeng.sdp
bfa1d71c49 feat: 定时任务拉取/检测变更dns.conf,更新客户端/etc/resolv.conf;定时任务拉取新版本更新;
refs #11
2025-10-11 15:52:49 +08:00
sundapeng.sdp
cb659f581b feat: 设置全局环境变量;集成fluent-bit安装包;设置组件健康检查目录;
refs #11
2025-10-11 15:52:49 +08:00
sundapeng.sdp
55f9317a1b feat: 用户侧安装组件前,提前下载全局配置文件并设置环境变量;集成 fluent-bit 安装包;
refs #11
2025-10-11 15:52:49 +08:00
sundapeng.sdp
f305a65abf feat: 打包分发程序目录调整
refs #11
2025-10-11 15:52:49 +08:00
sundapeng.sdp
46c34f3de6 feat: 定时解析argus-master产生的资源文件,生成动态配置文件,用于 Prometheus 热加载配置;
refs #9
2025-10-11 15:52:48 +08:00
sundapeng.sdp
8e2d751c2c fix: 修复启动服务失败导致的二次安装问题;修复缺少deps系统依赖问题;
refs #3
2025-10-11 15:52:48 +08:00
sundapeng.sdp
ab44edc22f feat: FTP镜像构建;加载vsftpd配置;兼容特定账户;配置共享目录;
refs #10
2025-10-11 15:52:48 +08:00
sundapeng.sdp
586740a17a feat: 基于算力平台的Prometheus镜像改造,supervisor自启应用;调整Prometheus.yml结构;
refs #9
2025-10-11 15:52:48 +08:00
sundapeng.sdp
5fa6e80578 feat: FTP服务器离线安装及配置;数据采集客户端支持一键部署、版本校验、组件健康检查、失败回滚等功能;
refs #3
2025-10-11 15:52:48 +08:00
sundapeng.sdp
e941537dd5 feat: 提交示例Node-Exporter安装包;
refs #3
2025-10-11 15:52:48 +08:00
sundapeng.sdp
4fb8652177 feat: 支持all-in-one模式构建、发布安装包;支持各组件健康状态监测;Prometheus相关配置文件;
refs #3
2025-10-11 15:52:48 +08:00
1e5e91b193 dev_1.0.0_yuyr_2:重新提交 PR,增加 master/agent 以及系统集成测试 (#17)
Reviewed-on: #17
Reviewed-by: sundapeng <sundp@mail.zgclab.edu.cn>
Reviewed-by: xuxt <xuxt@zgclab.edu.cn>
2025-10-11 15:04:46 +08:00
8a38d3d0b2 dev_1.0.0_yuyr 完成 log和bind模块开发部署测试 (#8)
- [x] 完成log模块镜像构建、本地端到端写日志——收集——查询流程;
- [x] 完成bind模块构建;
- [x] 内置域名IP自动更新脚本,使用 /private/argus/etc目录下文件进行同步,容器启动时自动写IP,定时任务刷新更新DNS服务器IP和DNS规则;

Co-authored-by: root <root@curious.host.com>
Reviewed-on: #8
Reviewed-by: sundapeng <sundp@mail.zgclab.edu.cn>
2025-09-22 16:39:38 +08:00
146 changed files with 13569 additions and 440 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea/

View File

@@ -5,3 +5,10 @@
项目文档:【腾讯文档】GPU集群运维系统
https://docs.qq.com/doc/DQUxDdmhIZ1dpeERk
## 构建账号配置
镜像构建和运行账号的 UID/GID 可通过 `configs/build_user.conf` 配置,详细说明见 `doc/build-user-config.md`。
## 本地端口占用提示
如需运行 BIND 模块端到端测试且宿主机 53 端口已占用,可通过环境变量 `HOST_DNS_PORT`(默认 1053)指定对外映射端口,例如 `HOST_DNS_PORT=12053 ./scripts/00_e2e_test.sh`。

View File

@@ -1,26 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
# 帮助信息
show_help() {
cat << EOF
cat <<'EOF'
ARGUS Unified Build System - Image Build Tool
Usage: $0 [OPTIONS]
Options:
--intranet Use intranet mirror for Ubuntu 22.04 packages
--intranet Use intranet mirror for log/bind builds
--master-offline Build master offline image (requires src/master/offline_wheels.tar.gz)
-h, --help Show this help message
Examples:
$0 # Build with default sources
$0 --intranet # Build with intranet mirror
$0 --master-offline # Additionally build argus-master:offline
$0 --intranet --master-offline
EOF
}
# 解析命令行参数
use_intranet=false
build_master=true
build_master_offline=false
while [[ $# -gt 0 ]]; do
case $1 in
@@ -28,48 +30,89 @@ while [[ $# -gt 0 ]]; do
use_intranet=true
shift
;;
--master)
build_master=true
shift
;;
--master-offline)
build_master=true
build_master_offline=true
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
echo "Unknown option: $1" >&2
show_help
exit 1
;;
esac
done
# 获取项目根目录
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
. "$root/scripts/common/build_user.sh"
declare -a build_args=()
if [[ "$use_intranet" == true ]]; then
build_args+=("--build-arg" "USE_INTRANET=true")
fi
cd "$root"
load_build_user
build_args+=("--build-arg" "ARGUS_BUILD_UID=${ARGUS_BUILD_UID}" "--build-arg" "ARGUS_BUILD_GID=${ARGUS_BUILD_GID}")
master_root="$root/src/master"
master_offline_tar="$master_root/offline_wheels.tar.gz"
master_offline_dir="$master_root/offline_wheels"
if [[ "$build_master_offline" == true ]]; then
if [[ ! -f "$master_offline_tar" ]]; then
echo "❌ offline wheels tar not found: $master_offline_tar" >&2
echo " 请提前准备好 offline_wheels.tar.gz 后再执行 --master-offline" >&2
exit 1
fi
echo "📦 Preparing offline wheels for master (extracting $master_offline_tar)"
rm -rf "$master_offline_dir"
mkdir -p "$master_offline_dir"
tar -xzf "$master_offline_tar" -C "$master_root"
has_wheel=$(find "$master_offline_dir" -maxdepth 1 -type f -name '*.whl' -print -quit)
if [[ -z "$has_wheel" ]]; then
echo "❌ offline_wheels extraction failed或无 wheel: $master_offline_dir" >&2
exit 1
fi
fi
echo "======================================="
echo "ARGUS Unified Build System"
echo "======================================="
if [[ "$use_intranet" == true ]]; then
echo "🌐 Mode: Intranet (Using internal mirror: 10.68.64.1)"
build_args="--build-arg USE_INTRANET=true"
else
echo "🌐 Mode: Public (Using default package sources)"
build_args=""
fi
echo "👤 Build user UID:GID -> ${ARGUS_BUILD_UID}:${ARGUS_BUILD_GID}"
echo "📁 Build context: $root"
echo ""
# 构建镜像的函数
build_image() {
local image_name=$1
local dockerfile_path=$2
local tag=$3
shift 3
local extra_args=("$@")
echo "🔄 Building $image_name image..."
echo " Dockerfile: $dockerfile_path"
echo " Tag: $tag"
if docker build $build_args -f "$dockerfile_path" -t "$tag" .; then
if docker build "${build_args[@]}" "${extra_args[@]}" -f "$dockerfile_path" -t "$tag" .; then
echo "✅ $image_name image built successfully"
return 0
else
@@ -78,11 +121,9 @@ build_image() {
fi
}
# 构建所有镜像
images_built=()
build_failed=false
# 构建 Elasticsearch 镜像
if build_image "Elasticsearch" "src/log/elasticsearch/build/Dockerfile" "argus-elasticsearch:latest"; then
images_built+=("argus-elasticsearch:latest")
else
@@ -91,7 +132,6 @@ fi
echo ""
# 构建 Kibana 镜像
if build_image "Kibana" "src/log/kibana/build/Dockerfile" "argus-kibana:latest"; then
images_built+=("argus-kibana:latest")
else
@@ -100,7 +140,6 @@ fi
echo ""
# 构建 BIND9 镜像
if build_image "BIND9" "src/bind/build/Dockerfile" "argus-bind9:latest"; then
images_built+=("argus-bind9:latest")
else
@@ -108,6 +147,30 @@ else
fi
echo ""
if [[ "$build_master" == true ]]; then
echo ""
echo "🔄 Building Master image..."
pushd "$master_root" >/dev/null
master_args=("--tag" "argus-master:latest")
if [[ "$use_intranet" == true ]]; then
master_args+=("--intranet")
fi
if [[ "$build_master_offline" == true ]]; then
master_args+=("--offline")
fi
if ./scripts/build_images.sh "${master_args[@]}"; then
if [[ "$build_master_offline" == true ]]; then
images_built+=("argus-master:offline")
else
images_built+=("argus-master:latest")
fi
else
build_failed=true
fi
popd >/dev/null
fi
echo "======================================="
echo "📦 Build Summary"
echo "======================================="
@@ -130,9 +193,13 @@ if [[ "$use_intranet" == true ]]; then
echo "🌐 Built with intranet mirror configuration"
fi
if [[ "$build_master_offline" == true ]]; then
echo ""
echo "🧳 Master offline wheels 已解压到 $master_offline_dir"
fi
echo ""
echo "🚀 Next steps:"
echo " cd src/log && ./scripts/save_images.sh # Export log images"
echo " cd src/bind && ./scripts/save_images.sh # Export bind images"
echo " cd src/log/tests && ./scripts/02_up.sh # Start log services"
echo " ./build/save_images.sh --compress # 导出镜像"
echo " cd src/master/tests && MASTER_IMAGE_TAG=argus-master:offline ./scripts/00_e2e_test.sh"
echo ""

View File

@@ -67,6 +67,7 @@ declare -A images=(
["argus-elasticsearch:latest"]="argus-elasticsearch-latest.tar"
["argus-kibana:latest"]="argus-kibana-latest.tar"
["argus-bind9:latest"]="argus-bind9-latest.tar"
["argus-master:offline"]="argus-master-offline.tar"
)
# 函数:检查镜像是否存在

2
configs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Local overrides for build user/group settings
build_user.local.conf

6
configs/build_user.conf Normal file
View File

@@ -0,0 +1,6 @@
# Default build-time UID/GID for Argus images
# Override by creating configs/build_user.local.conf with the same format.
# Syntax: KEY=VALUE, supports UID/GID only. Whitespace and lines starting with # are ignored.
UID=2133
GID=2015

38
doc/build-user-config.md Normal file
View File

@@ -0,0 +1,38 @@
# Argus 镜像构建 UID/GID 配置说明
通过统一配置文件可以为 Kibana、Elasticsearch、Bind、Master 等容器指定运行账号,解决跨机器部署时 UID/GID 不一致导致的权限问题。
## 配置入口
- 默认配置存放在 `configs/build_user.conf`,内容示例:
```bash
UID=2133
GID=2015
```
- 如果需要本地覆盖,可在 `configs/` 下新建 `build_user.local.conf`,字段与默认文件一致。该文件已列入 `.gitignore`,不会被意外提交。
- 亦可在执行脚本前通过环境变量 `ARGUS_BUILD_UID` / `ARGUS_BUILD_GID` 强制指定值,优先级最高。
## 作用范围
- `build/build_images.sh` 在构建 log/bind/master 镜像时读取配置,并传递 `--build-arg ARGUS_BUILD_UID/GID`;控制台会输出当前使用的 UID/GID。
- `src/master/scripts/build_images.sh` 同步使用配置,确保单独构建 master 镜像时行为一致。
- 各镜像 Dockerfile 会根据传入的 UID/GID 调整容器内账号(如 `elasticsearch`、`kibana`、`bind`、`argus`),并以环境变量形式暴露运行时可见值。
- Master 启动脚本会在执行 DNS 逻辑后,降权到配置的账号运行 `gunicorn`,确保写入 `/private/argus/**` 的文件具备正确属主。
- Log 模块测试脚本 `01_bootstrap.sh` 会根据配置修正挂载目录属主,方便端到端测试在任意用户下运行。
## 使用建议
1. 初次克隆仓库后无需修改,默认 UID/GID 保持向后兼容。
2. 如果在目标环境中使用新的账号(例如 `uid=4001,gid=4001`):
- 编辑 `configs/build_user.local.conf` 填入新值;
- 使用新账号登录,并确保其加入宿主机的 `docker` 组;
- 重新执行 `build/build_images.sh` 或相关模块的构建脚本。
3. 切换配置后建议重新运行目标模块的端到端脚本(如 `src/log/tests/scripts/01_bootstrap.sh`、`src/master/tests/scripts/00_e2e_test.sh`、`src/agent/tests/scripts/00_e2e_test.sh`),验证 `/private/argus` 下文件属主是否为期望账号。
## 故障排查
- **镜像构建报错 `groupmod: GID already in use`**:说明所选 GID 已存在于基础镜像,建议换用未占用的值,或在自定义基础镜像中先移除冲突。
- **容器内运行时报写权限不足**:检查宿主机挂载目录是否已经由目标 UID/GID 创建;必要时重新执行模块的 `01_bootstrap.sh` 之类的准备脚本。
- **仍看到旧 UID/GID**:确认脚本执行时未继承旧缓存,可运行 `ARGUS_BUILD_UID=... ARGUS_BUILD_GID=... ./build/build_images.sh` 强制覆盖。

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -euo pipefail
# Shared helper to load Argus build user/group configuration.
# Usage:
# source "${PROJECT_ROOT}/scripts/common/build_user.sh"
# load_build_user
# echo "$ARGUS_BUILD_UID:$ARGUS_BUILD_GID"
ARGUS_BUILD_UID_DEFAULT=2133
ARGUS_BUILD_GID_DEFAULT=2015
shopt -s extglob
_ARGUS_BUILD_USER_LOADED="${_ARGUS_BUILD_USER_LOADED:-0}"
_argus_build_user_script_dir() {
local dir
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "$dir"
}
argus_project_root() {
local script_dir
script_dir="$(_argus_build_user_script_dir)"
(cd "$script_dir/../.." >/dev/null && pwd)
}
_argus_trim() {
local value="$1"
value="${value##+([[:space:]])}"
value="${value%%+([[:space:]])}"
printf '%s' "$value"
}
_argus_is_number() {
[[ "$1" =~ ^[0-9]+$ ]]
}
load_build_user() {
if [[ "$_ARGUS_BUILD_USER_LOADED" == "1" ]]; then
return 0
fi
local project_root config_files config uid gid
project_root="$(argus_project_root)"
config_files=(
"$project_root/configs/build_user.local.conf"
"$project_root/configs/build_user.conf"
)
uid="$ARGUS_BUILD_UID_DEFAULT"
gid="$ARGUS_BUILD_GID_DEFAULT"
for config in "${config_files[@]}"; do
if [[ -f "$config" ]]; then
while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
local line key value
line="${raw_line%%#*}"
line="$(_argus_trim "${line}")"
[[ -z "$line" ]] && continue
if [[ "$line" != *=* ]]; then
echo "[ARGUS build_user] Ignoring malformed line in $config: $raw_line" >&2
continue
fi
key="${line%%=*}"
value="${line#*=}"
key="$(_argus_trim "$key")"
value="$(_argus_trim "$value")"
case "$key" in
UID)
uid="$value"
;;
GID)
gid="$value"
;;
*)
echo "[ARGUS build_user] Unknown key '$key' in $config" >&2
;;
esac
done < "$config"
break
fi
done
if [[ -n "${ARGUS_BUILD_UID:-}" ]]; then
uid="$ARGUS_BUILD_UID"
fi
if [[ -n "${ARGUS_BUILD_GID:-}" ]]; then
gid="$ARGUS_BUILD_GID"
fi
if ! _argus_is_number "$uid"; then
echo "[ARGUS build_user] Invalid UID '$uid'" >&2
return 1
fi
if ! _argus_is_number "$gid"; then
echo "[ARGUS build_user] Invalid GID '$gid'" >&2
return 1
fi
export ARGUS_BUILD_UID="$uid"
export ARGUS_BUILD_GID="$gid"
_ARGUS_BUILD_USER_LOADED=1
}
argus_build_user_args() {
load_build_user
printf '%s' "--build-arg ARGUS_BUILD_UID=${ARGUS_BUILD_UID} --build-arg ARGUS_BUILD_GID=${ARGUS_BUILD_GID}"
}
print_build_user() {
load_build_user
echo "ARGUS build user: UID=${ARGUS_BUILD_UID} GID=${ARGUS_BUILD_GID}"
}

5
src/agent/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
build/
*.egg-info/
__pycache__/
.env

View File

@@ -1,23 +0,0 @@
FROM python:3.11-slim
SHELL ["/bin/bash", "-c"]
ARG PIP_INDEX_URL=
ENV PIP_NO_CACHE_DIR=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app
WORKDIR /app
COPY requirements.txt ./
RUN set -euxo pipefail \
&& python -m pip install --upgrade pip \
&& if [[ -n "$PIP_INDEX_URL" ]]; then \
PIP_INDEX_URL="$PIP_INDEX_URL" python -m pip install -r requirements.txt; \
else \
python -m pip install -r requirements.txt; \
fi
COPY app ./app
CMD ["python", "-m", "app.main"]

View File

@@ -1,31 +1,66 @@
# Argus Agent Module
# Argus Agent 模块
Python agent that registers with the Argus master service, persists node information, gathers host metadata, reads module health files, and periodically reports status.
Argus Agent 是一个轻量级 Python 进程,负责向 Argus Master 注册节点、汇报健康数据,并维护本地持久化信息。模块现以 PyInstaller 打包为独立可执行文件,便于在普通容器或虚机中直接运行。
## Build & Run
## 构建可执行文件
```bash
cd src/agent
./scripts/build_images.sh # builds argus-agent:dev
./scripts/build_binary.sh # 生成 dist/argus-agent
```
Runtime expects a configuration file (generated by installer) at `/private/argus/agent/<hostname>/config`. Key fields:
脚本默认会在 Docker 容器 (`python:3.11-slim-bullseye`) 内执行 PyInstaller,确保产物运行时兼容 glibc 2.31+(覆盖 2.35 环境)。构建流程注意事项:
- `HOSTNAME`, `NODE_FILE`, `VERSION`
- `MASTER_ENDPOINT` (e.g. `http://master:3000`)
- `REPORT_INTERVAL_SECONDS`
- `SUBMODULE_HEALTH_FILE_DIR` (supports `{hostname}` placeholder)
- optional `GPU_NUMBER`
- 每次构建前会清理 `build/`、`dist/` 并在容器内重新创建虚拟环境。
- 需要使用内网 Python 镜像时,可通过 `PIP_INDEX_URL`、`PIP_EXTRA_INDEX_URL`、`PIP_TRUSTED_HOST` 等环境变量传入,脚本会自动透传给容器。
- 如果宿主机无法运行 Docker,可设置 `AGENT_BUILD_USE_DOCKER=0` 回退到本地构建;此时代码必须在 glibc ≤ 2.35 的机器上执行。
Health files live under `/private/argus/agent/health/<hostname>/` and must follow `<prefix>-*.json` naming (e.g. `log-fluentbit.json`). The agent sends parsed JSON objects keyed by file stem.
构建结束后脚本会在 `build/compat_check/` 下解包关键动态库并输出最高 `GLIBC_x.y` 版本,便于快速核对兼容性。如果结果中缺少 `libssl.so.3` / `libcrypto.so.3`,表示系统会在目标宿主机上使用本地 OpenSSL 库,无需额外处理。
## Tests
例如:
Docker-based E2E stack (master + agent):
```bash
strings build/compat_check/libpython*.so.1.0 | grep -Eo 'GLIBC_[0-9]+\.[0-9]+' | sort -Vu | tail -n1
```
如遇构建失败,常见原因是 Docker 不可用(请改用 `AGENT_BUILD_USE_DOCKER=0`)或无法访问 Python 包镜像(先设置上述镜像环境变量后重试)。
## 运行时配置
Agent 不再依赖配置文件;所有参数均由环境变量与主机名推导:
| 变量 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `MASTER_ENDPOINT` | 是 | N/A | Master 基础地址,可写 `http://host:3000` 或 `host:3000`(自动补全 `http://`)。 |
| `REPORT_INTERVAL_SECONDS` | 否 | `60` | 状态上报间隔(秒)。必须为正整数。 |
| `AGENT_HOSTNAME` | 否 | `$(hostname)` | 覆盖容器内主机名,便于测试或特殊命名需求。 |
派生路径:
- 节点信息:`/private/argus/agent/<hostname>/node.json`
- 子模块健康目录:`/private/argus/agent/<hostname>/health/`
健康目录中的文件需遵循 `<模块前缀>-*.json` 命名(例如 `log-fluentbit.json`、`metric-node-exporter.json`),文件内容会原样并入上报的 `health` 字段。
## 日志与持久化
- Agent 会在成功注册、状态上报、异常重试等关键节点输出结构化日志,便于聚合分析。
- `node.json` 保存 Master 返回的最新节点对象,用于重启后继续使用既有节点 ID。
## 端到端测试
仓库内提供 Docker Compose 测试栈(master + ubuntu 容器):
```bash
cd src/agent/tests
./scripts/00_e2e_test.sh
```
The scripts provision configs/health directories under `tests/private/` and clean up via `07_down.sh`.
测试脚本会:
1. 构建 master 镜像与 agent 可执行文件。
2. 以 `ubuntu:24.04` 启动 agent 容器,并通过环境变量注入 `MASTER_ENDPOINT`、`REPORT_INTERVAL_SECONDS`。
3. 验证注册、健康上报、nodes.json 生成、统计接口,以及“容器重启 + IP 变化”重注册流程。
4. 清理 `tests/private/` 与临时容器网络。
如需在真实环境部署,只需将 `dist/argus-agent` 连同健康目录挂载到目标主机,并按上表设置环境变量即可。

View File

@@ -27,7 +27,7 @@ def collect_metadata(config: AgentConfig) -> Dict[str, Any]:
"instance": instance,
"cpu_number": _detect_cpu_count(),
"memory_in_bytes": _detect_memory_bytes(),
"gpu_number": _detect_gpu_count(config),
"gpu_number": _detect_gpu_count(),
}
return meta
@@ -70,11 +70,8 @@ def _detect_memory_bytes() -> int:
return 0
def _detect_gpu_count(config: AgentConfig) -> int:
"""采集 GPU 数量,可被配置覆盖。"""
if config.gpu_number_override is not None:
return config.gpu_number_override
def _detect_gpu_count() -> int:
"""采集 GPU 数量,如无法探测则默认为 0。"""
try:
proc = subprocess.run(
["nvidia-smi", "-L"],

View File

@@ -1,8 +1,14 @@
from __future__ import annotations
import os
import socket
from dataclasses import dataclass
from pathlib import Path
from typing import Final
from .version import VERSION
DEFAULT_REPORT_INTERVAL_SECONDS: Final[int] = 60
@dataclass(frozen=True)
@@ -12,90 +18,57 @@ class AgentConfig:
version: str
master_endpoint: str
report_interval_seconds: int
health_dir_template: str
gpu_number_override: int | None
health_dir: str
request_timeout_seconds: int = 10
@property
def health_dir(self) -> str:
return self.health_dir_template.format(hostname=self.hostname)
def _normalise_master_endpoint(value: str) -> str:
value = value.strip()
if not value:
raise ValueError("MASTER_ENDPOINT environment variable is required")
if not value.startswith("http://") and not value.startswith("https://"):
value = f"http://{value}"
return value.rstrip("/")
def _parse_config_file(path: str) -> dict[str, str]:
result: dict[str, str] = {}
def _read_report_interval(raw_value: str | None) -> int:
if raw_value is None or raw_value.strip() == "":
return DEFAULT_REPORT_INTERVAL_SECONDS
try:
with open(path, "r", encoding="utf-8") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
result[key.strip().upper()] = value.strip()
except FileNotFoundError:
raise FileNotFoundError(f"Agent config file not found: {path}") from None
return result
def load_config(path: str) -> AgentConfig:
"""读取配置文件并结合环境变量,返回 AgentConfig。"""
config_values = _parse_config_file(path)
force_env = os.environ.get("AGENT_FORCE_ENV", "0").lower() in {"1", "true", "yes"}
def read_key(key: str, default: str | None = None, *, required: bool = False) -> str:
env_key = f"AGENT_{key}"
if env_key in os.environ:
return os.environ[env_key]
if force_env and key in os.environ:
return os.environ[key]
if key in config_values:
return config_values[key]
if default is not None:
return default
if required:
raise ValueError(f"Missing required configuration key: {key}")
return ""
hostname = read_key("HOSTNAME", required=True)
node_file = read_key("NODE_FILE", f"/private/argus/agent/{hostname}/node.json")
version = read_key("VERSION", "1.0.0")
master_endpoint = read_key("MASTER_ENDPOINT", required=True)
report_interval_raw = read_key("REPORT_INTERVAL_SECONDS", "60")
health_dir_template = read_key(
"SUBMODULE_HEALTH_FILE_DIR",
f"/private/argus/agent/health/{{hostname}}/",
)
gpu_override_raw = read_key("GPU_NUMBER", "")
try:
report_interval_seconds = int(report_interval_raw)
interval = int(raw_value)
except ValueError as exc:
raise ValueError("REPORT_INTERVAL_SECONDS must be an integer") from exc
if report_interval_seconds <= 0:
if interval <= 0:
raise ValueError("REPORT_INTERVAL_SECONDS must be positive")
return interval
gpu_override = None
if gpu_override_raw:
try:
gpu_override = int(gpu_override_raw)
except ValueError as exc:
raise ValueError("GPU_NUMBER must be an integer when provided") from exc
if gpu_override < 0:
raise ValueError("GPU_NUMBER must be non-negative")
if not master_endpoint.startswith("http://") and not master_endpoint.startswith("https://"):
master_endpoint = f"http://{master_endpoint}"
def _resolve_hostname() -> str:
return os.environ.get("AGENT_HOSTNAME") or socket.gethostname()
def load_config() -> AgentConfig:
"""从环境变量推导配置,移除了外部配置文件依赖。"""
hostname = _resolve_hostname()
node_file = f"/private/argus/agent/{hostname}/node.json"
health_dir = f"/private/argus/agent/{hostname}/health/"
master_endpoint_env = os.environ.get("MASTER_ENDPOINT")
if master_endpoint_env is None:
raise ValueError("MASTER_ENDPOINT environment variable is not set")
master_endpoint = _normalise_master_endpoint(master_endpoint_env)
report_interval_seconds = _read_report_interval(os.environ.get("REPORT_INTERVAL_SECONDS"))
Path(node_file).parent.mkdir(parents=True, exist_ok=True)
Path(health_dir_template.format(hostname=hostname)).mkdir(parents=True, exist_ok=True)
Path(health_dir).mkdir(parents=True, exist_ok=True)
return AgentConfig(
hostname=hostname,
node_file=node_file,
version=version,
master_endpoint=master_endpoint.rstrip("/"),
version=VERSION,
master_endpoint=master_endpoint,
report_interval_seconds=report_interval_seconds,
health_dir_template=health_dir_template,
gpu_number_override=gpu_override,
health_dir=health_dir,
)

View File

@@ -1,8 +1,6 @@
from __future__ import annotations
import argparse
import signal
import sys
import time
from datetime import datetime, timezone
from typing import Optional
@@ -32,28 +30,15 @@ class StopSignal:
return self._stop
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Argus agent")
parser.add_argument(
"--config",
dest="config_path",
default=None,
help="Path to agent config file",
)
return parser.parse_args(argv)
def main(argv: Optional[list[str]] = None) -> int:
def main(argv: Optional[list[str]] = None) -> int: # noqa: ARG001 - 保留签名以兼容入口调用
setup_logging()
args = parse_args(argv or sys.argv[1:])
stop_signal = StopSignal()
signal.signal(signal.SIGTERM, stop_signal.set)
signal.signal(signal.SIGINT, stop_signal.set)
try:
config_path = args.config_path or _default_config_path()
config = load_config(config_path)
config = load_config()
except Exception as exc:
LOGGER.error("Failed to load configuration", extra={"error": str(exc)})
return 1
@@ -89,13 +74,6 @@ def main(argv: Optional[list[str]] = None) -> int:
return 0
def _default_config_path() -> str:
from socket import gethostname
hostname = gethostname()
return f"/private/argus/agent/{hostname}/config"
def _register_with_retry(
client: AgentClient,
config: AgentConfig,

69
src/agent/app/version.py Normal file
View File

@@ -0,0 +1,69 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import Optional
import importlib.metadata
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover
import tomli as tomllib # type: ignore[no-redef]
def _candidate_paths() -> list[Path]:
paths = []
bundle_dir: Optional[str] = getattr(sys, "_MEIPASS", None)
if bundle_dir:
paths.append(Path(bundle_dir) / "pyproject.toml")
paths.append(Path(__file__).resolve().parent.parent / "pyproject.toml")
paths.append(Path(__file__).resolve().parent / "pyproject.toml")
paths.append(Path.cwd() / "pyproject.toml")
return paths
def _read_from_pyproject() -> Optional[str]:
for path in _candidate_paths():
if not path.exists():
continue
try:
with path.open("rb") as handle:
data = tomllib.load(handle)
except (OSError, tomllib.TOMLDecodeError):
continue
project = data.get("project")
if isinstance(project, dict):
version = project.get("version")
if isinstance(version, str):
return version
tool = data.get("tool")
if isinstance(tool, dict):
argus_cfg = tool.get("argus")
if isinstance(argus_cfg, dict):
version = argus_cfg.get("version")
if isinstance(version, str):
return version
return None
def _detect_version() -> str:
try:
return importlib.metadata.version("argus-agent")
except importlib.metadata.PackageNotFoundError:
pass
override = os.environ.get("AGENT_VERSION_OVERRIDE")
if override:
return override
fallback = _read_from_pyproject()
if fallback:
return fallback
return "0.0.0"
VERSION: str = _detect_version()
def get_version() -> str:
return VERSION

BIN
src/agent/dist/argus-agent vendored Executable file

Binary file not shown.

10
src/agent/entry.py Normal file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from app.main import main as agent_main
if __name__ == "__main__":
sys.exit(agent_main())

19
src/agent/pyproject.toml Normal file
View File

@@ -0,0 +1,19 @@
[project]
name = "argus-agent"
version = "1.1.0"
description = "Argus agent binary"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"requests==2.31.0"
]
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[tool.argus]
entry = "app.main:main"
[tool.setuptools]
packages = ["app"]

View File

@@ -1 +0,0 @@
requests==2.31.0

View File

@@ -0,0 +1,690 @@
#!/usr/bin/env bash
set -euo pipefail
LOG_PREFIX="[AGENT-VERIFY]"
MASTER_ENDPOINT_DEFAULT=""
AGENT_DATA_ROOT_DEFAULT="/private/argus/agent"
AGENT_ETC_ROOT_DEFAULT="/private/argus/etc"
REPORT_INTERVAL_DEFAULT="2"
ALLOW_CONFIG_TOUCH="false"
KEEP_TEST_HEALTH="false"
log_info() {
echo "${LOG_PREFIX} INFO $*"
}
log_warn() {
echo "${LOG_PREFIX} WARN $*" >&2
}
log_error() {
echo "${LOG_PREFIX} ERROR $*" >&2
}
usage() {
cat <<'USAGE'
Usage: agent_deployment_verify.sh [options]
Options:
--allow-config-touch Enable optional config PUT dry-run check.
--keep-test-health Keep the temporary verify health file after checks.
-h, --help Show this help message.
Environment variables:
MASTER_ENDPOINT (required) Master API base endpoint, e.g. http://master:3000
AGENT_DATA_ROOT (default: /private/argus/agent)
AGENT_ETC_ROOT (default: /private/argus/etc)
VERIFY_HOSTNAME (default: output of hostname)
REPORT_INTERVAL_SECONDS (default: 2) Agent report interval in seconds
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--allow-config-touch)
ALLOW_CONFIG_TOUCH="true"
shift
;;
--keep-test-health)
KEEP_TEST_HEALTH="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
log_error "Unknown option: $1"
usage >&2
exit 2
;;
esac
done
MASTER_ENDPOINT="${MASTER_ENDPOINT:-$MASTER_ENDPOINT_DEFAULT}"
AGENT_DATA_ROOT="${AGENT_DATA_ROOT:-$AGENT_DATA_ROOT_DEFAULT}"
AGENT_ETC_ROOT="${AGENT_ETC_ROOT:-$AGENT_ETC_ROOT_DEFAULT}"
VERIFY_HOSTNAME="${VERIFY_HOSTNAME:-$(hostname)}"
REPORT_INTERVAL_SECONDS="${REPORT_INTERVAL_SECONDS:-$REPORT_INTERVAL_DEFAULT}"
if [[ -z "$MASTER_ENDPOINT" ]]; then
log_error "MASTER_ENDPOINT is required"
exit 2
fi
if ! [[ "$REPORT_INTERVAL_SECONDS" =~ ^[0-9]+$ ]] || [[ "$REPORT_INTERVAL_SECONDS" -le 0 ]]; then
log_warn "Invalid REPORT_INTERVAL_SECONDS='$REPORT_INTERVAL_SECONDS', fallback to $REPORT_INTERVAL_DEFAULT"
REPORT_INTERVAL_SECONDS="$REPORT_INTERVAL_DEFAULT"
fi
normalize_endpoint() {
local endpoint="$1"
if [[ "$endpoint" != http://* && "$endpoint" != https://* ]]; then
endpoint="http://$endpoint"
fi
endpoint="${endpoint%/}"
echo "$endpoint"
}
MASTER_BASE="$(normalize_endpoint "$MASTER_ENDPOINT")"
NODE_DIR="$AGENT_DATA_ROOT/$VERIFY_HOSTNAME"
NODE_JSON="$NODE_DIR/node.json"
HEALTH_DIR="$NODE_DIR/health"
DNS_CONF="$AGENT_ETC_ROOT/dns.conf"
UPDATE_SCRIPT="$AGENT_ETC_ROOT/update-dns.sh"
declare -a RESULTS_PASS=()
declare -a RESULTS_WARN=()
declare -a RESULTS_FAIL=()
add_result() {
local level="$1" message="$2"
case "$level" in
PASS)
RESULTS_PASS+=("$message")
log_info "$message"
;;
WARN)
RESULTS_WARN+=("$message")
log_warn "$message"
;;
FAIL)
RESULTS_FAIL+=("$message")
log_error "$message"
;;
esac
}
HAS_JQ="0"
if command -v jq >/dev/null 2>&1; then
HAS_JQ="1"
fi
if ! command -v curl >/dev/null 2>&1; then
log_error "curl command not found; please install curl (e.g. apt-get install -y curl)"
exit 2
fi
if [[ "$HAS_JQ" == "0" ]] && ! command -v python3 >/dev/null 2>&1; then
log_error "Neither jq nor python3 is available for JSON processing"
exit 2
fi
CURL_OPTS=(--fail --show-error --silent --max-time 10)
curl_json() {
local url="$1"
if ! curl "${CURL_OPTS[@]}" "$url"; then
return 1
fi
}
json_query() {
local json="$1" jq_expr="$2" py_expr="$3"
if [[ "$HAS_JQ" == "1" ]]; then
if ! output=$(printf '%s' "$json" | jq -e -r "$jq_expr" 2>/dev/null); then
return 1
fi
printf '%s' "$output"
return 0
fi
python3 - "$py_expr" <<'PY'
import json
import sys
expr = sys.argv[1]
try:
data = json.load(sys.stdin)
value = eval(expr, {}, {"data": data})
except Exception:
sys.exit(1)
if value is None:
sys.exit(1)
if isinstance(value, (dict, list)):
print(json.dumps(value))
else:
print(value)
PY
}
json_length() {
local json="$1" jq_expr="$2" py_expr="$3"
if [[ "$HAS_JQ" == "1" ]]; then
if ! output=$(printf '%s' "$json" | jq -e "$jq_expr" 2>/dev/null); then
return 1
fi
printf '%s' "$output"
return 0
fi
python3 - "$py_expr" <<'PY'
import json
import sys
expr = sys.argv[1]
try:
data = json.load(sys.stdin)
value = eval(expr, {}, {"data": data})
except Exception:
sys.exit(1)
try:
print(len(value))
except Exception:
sys.exit(1)
PY
}
json_has_key() {
local json="$1" jq_expr="$2" py_expr="$3"
if [[ "$HAS_JQ" == "1" ]]; then
if printf '%s' "$json" | jq -e "$jq_expr" >/dev/null 2>&1; then
return 0
fi
return 1
fi
python3 - "$py_expr" <<'PY'
import json
import sys
expr = sys.argv[1]
try:
data = json.load(sys.stdin)
value = eval(expr, {}, {"data": data})
except Exception:
sys.exit(1)
if value:
sys.exit(0)
sys.exit(1)
PY
}
iso_to_epoch() {
local value="$1"
if command -v date >/dev/null 2>&1; then
date -d "$value" +%s 2>/dev/null && return 0
fi
if command -v python3 >/dev/null 2>&1; then
python3 - "$value" <<'PY'
import sys
from datetime import datetime
value = sys.argv[1]
if value is None or value == "":
sys.exit(1)
if value.endswith('Z'):
value = value[:-1] + '+00:00'
try:
dt = datetime.fromisoformat(value)
except ValueError:
sys.exit(1)
print(int(dt.timestamp()))
PY
return $?
fi
return 1
}
validate_json_file() {
local path="$1"
if [[ "$HAS_JQ" == "1" ]]; then
jq empty "$path" >/dev/null 2>&1 && return 0
return 1
fi
if command -v python3 >/dev/null 2>&1; then
python3 - "$path" <<'PY'
import json
import sys
path = sys.argv[1]
with open(path, 'r', encoding='utf-8') as handle:
json.load(handle)
PY
return $?
fi
return 0
}
ensure_directory() {
local dir="$1"
if [[ ! -d "$dir" ]]; then
log_warn "Creating missing directory $dir"
mkdir -p "$dir"
fi
}
TEST_HEALTH_FILE=""
TEST_HEALTH_BACKUP=""
TEST_HEALTH_EXISTED="false"
cleanup() {
if [[ -n "$TEST_HEALTH_FILE" ]]; then
if [[ "$TEST_HEALTH_EXISTED" == "true" ]]; then
printf '%s' "$TEST_HEALTH_BACKUP" > "$TEST_HEALTH_FILE"
elif [[ "$KEEP_TEST_HEALTH" == "true" ]]; then
:
else
rm -f "$TEST_HEALTH_FILE"
fi
fi
}
trap cleanup EXIT
log_info "Starting agent deployment verification for hostname '$VERIFY_HOSTNAME'"
# 4.2 Master health checks
health_resp=""
if ! health_resp=$(curl "${CURL_OPTS[@]}" -w '\n%{http_code} %{time_total}' "$MASTER_BASE/healthz" 2>/tmp/agent_verify_healthz.err); then
error_detail=$(cat /tmp/agent_verify_healthz.err || true)
add_result FAIL "GET /healthz failed: $error_detail"
else
http_meta=$(tail -n1 <<<"$health_resp")
payload=$(head -n -1 <<<"$health_resp" || true)
status_code=${http_meta%% *}
elapsed=${http_meta##* }
add_result PASS "GET /healthz status=$status_code elapsed=${elapsed}s payload=$payload"
fi
rm -f /tmp/agent_verify_healthz.err
if ! readyz_resp=$(curl "${CURL_OPTS[@]}" -w '\n%{http_code} %{time_total}' "$MASTER_BASE/readyz" 2>/tmp/agent_verify_readyz.err); then
error_detail=$(cat /tmp/agent_verify_readyz.err || true)
add_result FAIL "GET /readyz failed: $error_detail"
readyz_payload=""
else
readyz_meta=$(tail -n1 <<<"$readyz_resp")
readyz_payload=$(head -n -1 <<<"$readyz_resp" || true)
readyz_status=${readyz_meta%% *}
readyz_elapsed=${readyz_meta##* }
add_result PASS "GET /readyz status=$readyz_status elapsed=${readyz_elapsed}s"
fi
rm -f /tmp/agent_verify_readyz.err
# 4.3 Nodes list and detail
if ! nodes_json=$(curl_json "$MASTER_BASE/api/v1/master/nodes" 2>/tmp/agent_verify_nodes.err); then
error_detail=$(cat /tmp/agent_verify_nodes.err || true)
add_result FAIL "GET /api/v1/master/nodes failed: $error_detail"
nodes_json=""
fi
rm -f /tmp/agent_verify_nodes.err
NODE_ENTRY=""
NODE_ID=""
NODE_IP=""
if [[ -n "$nodes_json" ]]; then
if [[ "$HAS_JQ" == "1" ]]; then
NODE_ENTRY=$(printf '%s' "$nodes_json" | jq -e --arg name "$VERIFY_HOSTNAME" '.[] | select(.name == $name)') || NODE_ENTRY=""
else
NODE_ENTRY=$(python3 - "$VERIFY_HOSTNAME" <<'PY'
import json
import sys
hostname = sys.argv[1]
nodes = json.load(sys.stdin)
for node in nodes:
if node.get("name") == hostname:
import json as _json
print(_json.dumps(node))
sys.exit(0)
sys.exit(1)
PY
) || NODE_ENTRY=""
fi
if [[ -z "$NODE_ENTRY" ]]; then
add_result FAIL "Current node '$VERIFY_HOSTNAME' not found in master nodes list"
else
if NODE_ID=$(json_query "$NODE_ENTRY" '.id' 'data["id"]'); then
add_result PASS "Discovered node id '$NODE_ID' for hostname '$VERIFY_HOSTNAME'"
else
add_result FAIL "Failed to extract node id from master response"
fi
fi
if [[ -n "$NODE_ENTRY" ]] && NODE_DETAIL=$(curl_json "$MASTER_BASE/api/v1/master/nodes/$NODE_ID" 2>/tmp/agent_verify_node_detail.err); then
NODE_DETAIL_JSON="$NODE_DETAIL"
add_result PASS "Fetched node detail for $NODE_ID"
if NODE_IP=$(json_query "$NODE_DETAIL_JSON" '.meta_data.ip // .meta_data.host_ip // empty' 'data.get("meta_data", {}).get("ip") or data.get("meta_data", {}).get("host_ip") or ""'); then
if [[ -n "$NODE_IP" ]]; then
add_result PASS "Registered node IP=$NODE_IP"
else
add_result INFO "Node detail does not expose IP fields"
fi
fi
else
error_detail=$(cat /tmp/agent_verify_node_detail.err 2>/dev/null || true)
add_result FAIL "Failed to fetch node detail for $NODE_ID: $error_detail"
NODE_DETAIL_JSON=""
fi
rm -f /tmp/agent_verify_node_detail.err
if stats_json=$(curl_json "$MASTER_BASE/api/v1/master/nodes/statistics" 2>/tmp/agent_verify_stats.err); then
if total_nodes=$(json_query "$stats_json" '.total // .total_nodes' 'data.get("total") or data.get("total_nodes")'); then
if [[ "$total_nodes" =~ ^[0-9]+$ ]] && [[ "$total_nodes" -ge 1 ]]; then
add_result PASS "Statistics total=$total_nodes"
else
add_result WARN "Statistics total field not numeric: $total_nodes"
fi
else
add_result WARN "Unable to read total field from statistics"
fi
active_nodes=""
if [[ "$HAS_JQ" == "1" ]]; then
active_nodes=$(printf '%s' "$stats_json" | jq -e 'if .status_statistics then (.status_statistics[] | select(.status == "online") | .count) else empty end' 2>/dev/null | head -n1 || true)
elif command -v python3 >/dev/null 2>&1; then
active_nodes=$(printf '%s' "$stats_json" | python3 -c 'import json,sys; data=json.load(sys.stdin); print(next((row.get("count") for row in data.get("status_statistics", []) if row.get("status")=="online"), ""))' 2>/dev/null)
fi
if [[ -n "$active_nodes" ]]; then
add_result PASS "Online nodes reported by master: $active_nodes"
fi
if [[ "$HAS_JQ" == "1" ]]; then
node_count=$(printf '%s' "$nodes_json" | jq 'length')
else
node_count=$(json_length "$nodes_json" 'length' 'len(data)')
fi
if [[ "$total_nodes" =~ ^[0-9]+$ ]] && [[ "$node_count" =~ ^[0-9]+$ ]] && [[ "$total_nodes" -lt "$node_count" ]]; then
add_result WARN "Statistics total=$total_nodes less than nodes list count=$node_count"
fi
else
error_detail=$(cat /tmp/agent_verify_stats.err 2>/dev/null || true)
add_result FAIL "Failed to fetch node statistics: $error_detail"
fi
rm -f /tmp/agent_verify_stats.err
else
NODE_DETAIL_JSON=""
fi
# 4.4 Agent persistence checks
if [[ -f "$NODE_JSON" ]]; then
node_file_content="$(cat "$NODE_JSON")"
if node_id_local=$(json_query "$node_file_content" '.id' 'data["id"]'); then
if [[ "$NODE_ID" != "" && "$node_id_local" == "$NODE_ID" ]]; then
add_result PASS "node.json id matches master ($NODE_ID)"
else
add_result FAIL "node.json id '$node_id_local' differs from master id '$NODE_ID'"
fi
else
add_result FAIL "Unable to extract id from node.json"
fi
if node_name_local=$(json_query "$node_file_content" '.name' 'data["name"]'); then
if [[ "$node_name_local" == "$VERIFY_HOSTNAME" ]]; then
add_result PASS "node.json name matches $VERIFY_HOSTNAME"
else
add_result FAIL "node.json name '$node_name_local' differs from hostname '$VERIFY_HOSTNAME'"
fi
else
add_result FAIL "Unable to extract name from node.json"
fi
if register_time=$(json_query "$node_file_content" '.register_time' 'data.get("register_time")'); then
if iso_to_epoch "$register_time" >/dev/null 2>&1; then
add_result PASS "node.json register_time valid ISO timestamp"
else
add_result WARN "node.json register_time invalid: $register_time"
fi
else
add_result WARN "node.json missing register_time"
fi
if last_updated=$(json_query "$node_file_content" '.last_updated' 'data.get("last_updated")'); then
if iso_to_epoch "$last_updated" >/dev/null 2>&1; then
add_result PASS "node.json last_updated valid ISO timestamp"
else
add_result WARN "node.json last_updated invalid: $last_updated"
fi
else
add_result WARN "node.json missing last_updated"
fi
else
add_result FAIL "node.json not found at $NODE_JSON"
node_file_content=""
fi
ensure_directory "$HEALTH_DIR"
if [[ -d "$HEALTH_DIR" ]]; then
shopt -s nullglob
health_files=("$HEALTH_DIR"/*.json)
shopt -u nullglob
if [[ ${#health_files[@]} -eq 0 ]]; then
add_result WARN "Health directory $HEALTH_DIR is empty"
else
for hf in "${health_files[@]}"; do
base=$(basename "$hf")
if [[ "$base" != *-* ]]; then
add_result WARN "Health file $base does not follow <module>-*.json"
continue
fi
if ! validate_json_file "$hf" >/dev/null 2>&1; then
add_result WARN "Health file $base is not valid JSON"
fi
done
fi
else
add_result WARN "Health directory $HEALTH_DIR missing"
fi
if getent hosts master.argus.com >/dev/null 2>&1; then
resolved_ips=$(getent hosts master.argus.com | awk '{print $1}' | xargs)
add_result PASS "master.argus.com resolves to $resolved_ips"
else
add_result FAIL "Failed to resolve master.argus.com"
fi
# 4.5 Master-Node status consistency
sleep_interval=$((REPORT_INTERVAL_SECONDS + 2))
if [[ -n "$NODE_DETAIL_JSON" ]]; then
detail_pre="$NODE_DETAIL_JSON"
else
detail_pre=""
fi
if [[ -z "$detail_pre" && -n "$NODE_ID" ]]; then
if detail_pre=$(curl_json "$MASTER_BASE/api/v1/master/nodes/$NODE_ID" 2>/tmp/agent_verify_detail_pre.err); then
add_result PASS "Fetched node detail pre-check"
else
error_detail=$(cat /tmp/agent_verify_detail_pre.err 2>/dev/null || true)
add_result FAIL "Unable to fetch node detail for status check: $error_detail"
fi
rm -f /tmp/agent_verify_detail_pre.err
fi
server_ts_pre=""
agent_ts_pre=""
server_ts_post=""
agent_ts_post=""
if [[ -n "$detail_pre" ]]; then
server_ts_pre=$(json_query "$detail_pre" '.last_report' 'data.get("last_report")' || echo "")
agent_ts_pre=$(json_query "$detail_pre" '.agent_last_report' 'data.get("agent_last_report")' || echo "")
log_info "Captured initial last_report timestamps server='$server_ts_pre' agent='$agent_ts_pre'"
sleep "$sleep_interval"
if detail_post=$(curl_json "$MASTER_BASE/api/v1/master/nodes/$NODE_ID" 2>/tmp/agent_verify_detail_post.err); then
server_ts_post=$(json_query "$detail_post" '.last_report' 'data.get("last_report")' || echo "")
agent_ts_post=$(json_query "$detail_post" '.agent_last_report' 'data.get("agent_last_report")' || echo "")
if [[ "$server_ts_post" != "$server_ts_pre" ]]; then
add_result PASS "last_report.server_timestamp advanced (pre=$server_ts_pre post=$server_ts_post)"
else
add_result FAIL "last_report.server_timestamp did not change after ${sleep_interval}s"
fi
if [[ "$agent_ts_post" != "$agent_ts_pre" ]]; then
add_result PASS "last_report.agent_timestamp advanced"
else
add_result FAIL "last_report.agent_timestamp did not change"
fi
if [[ -n "$node_file_content" ]]; then
if node_last_updated=$(json_query "$node_file_content" '.last_updated' 'data.get("last_updated")'); then
if epoch_post=$(iso_to_epoch "$server_ts_post" 2>/dev/null); then
if node_epoch=$(iso_to_epoch "$node_last_updated" 2>/dev/null); then
diff=$((epoch_post - node_epoch))
[[ $diff -lt 0 ]] && diff=$((-diff))
tolerance=$((REPORT_INTERVAL_SECONDS * 2))
if [[ $diff -le $tolerance ]]; then
add_result PASS "last_report.server_timestamp and node.json last_updated within tolerance ($diff s)"
else
add_result WARN "Timestamp gap between master ($server_ts_post) and node.json ($node_last_updated) is ${diff}s"
fi
fi
fi
fi
fi
NODE_DETAIL_JSON="$detail_post"
else
error_detail=$(cat /tmp/agent_verify_detail_post.err 2>/dev/null || true)
add_result FAIL "Failed to fetch node detail post-check: $error_detail"
fi
rm -f /tmp/agent_verify_detail_post.err
fi
# 4.6 Health simulation
TEST_HEALTH_FILE="$HEALTH_DIR/verify-master.json"
ensure_directory "$HEALTH_DIR"
if [[ -f "$TEST_HEALTH_FILE" ]]; then
TEST_HEALTH_EXISTED="true"
TEST_HEALTH_BACKUP="$(cat "$TEST_HEALTH_FILE")"
else
TEST_HEALTH_EXISTED="false"
fi
create_health_file() {
local message="$1"
cat > "$TEST_HEALTH_FILE" <<HEALTHJSON
{"status":"ok","message":"$message"}
HEALTHJSON
}
validate_health_in_master() {
local expected_message="$1"
local detail_json="$2"
local message
if message=$(json_query "$detail_json" '.health["verify-master"].message' 'data.get("health", {}).get("verify-master", {}).get("message")'); then
if [[ "$message" == "$expected_message" ]]; then
return 0
fi
fi
return 1
}
remove_health_from_master() {
local detail_json="$1"
if json_has_key "$detail_json" '(.health | has("verify-master"))' '"verify-master" in data.get("health", {})'; then
return 1
fi
return 0
}
health_message_one="verify $(date +%s)"
create_health_file "$health_message_one"
add_result PASS "Created test health file $TEST_HEALTH_FILE"
sleep "$sleep_interval"
if detail_health_one=$(curl_json "$MASTER_BASE/api/v1/master/nodes/$NODE_ID" 2>/tmp/agent_verify_health1.err); then
if validate_health_in_master "$health_message_one" "$detail_health_one"; then
add_result PASS "Master reflects verify-master health message"
else
add_result FAIL "Master health payload does not match test message"
fi
else
error_detail=$(cat /tmp/agent_verify_health1.err 2>/dev/null || true)
add_result FAIL "Failed to fetch node detail during health validation: $error_detail"
detail_health_one=""
fi
rm -f /tmp/agent_verify_health1.err
health_message_two="verify $(date +%s)-update"
create_health_file "$health_message_two"
sleep "$sleep_interval"
if detail_health_two=$(curl_json "$MASTER_BASE/api/v1/master/nodes/$NODE_ID" 2>/tmp/agent_verify_health2.err); then
if validate_health_in_master "$health_message_two" "$detail_health_two"; then
add_result PASS "Master health updated to new message"
else
add_result FAIL "Master health message did not update"
fi
else
error_detail=$(cat /tmp/agent_verify_health2.err 2>/dev/null || true)
add_result FAIL "Failed to fetch node detail after health update: $error_detail"
detail_health_two=""
fi
rm -f /tmp/agent_verify_health2.err
rm -f "$TEST_HEALTH_FILE"
sleep "$sleep_interval"
if detail_health_three=$(curl_json "$MASTER_BASE/api/v1/master/nodes/$NODE_ID" 2>/tmp/agent_verify_health3.err); then
if remove_health_from_master "$detail_health_three"; then
add_result PASS "Master health no longer lists verify-master after removal"
else
add_result FAIL "Master health still contains verify-master after file deletion"
fi
else
error_detail=$(cat /tmp/agent_verify_health3.err 2>/dev/null || true)
add_result FAIL "Failed to fetch node detail after health removal: $error_detail"
fi
rm -f /tmp/agent_verify_health3.err
if [[ "$TEST_HEALTH_EXISTED" == "true" ]]; then
printf '%s' "$TEST_HEALTH_BACKUP" > "$TEST_HEALTH_FILE"
fi
# Optional config touch
if [[ "$ALLOW_CONFIG_TOUCH" == "true" ]]; then
if [[ -n "$NODE_ID" ]]; then
payload='{"label": {"verify": "true"}}'
if curl "${CURL_OPTS[@]}" -X PUT -H 'Content-Type: application/json' -d "$payload" "$MASTER_BASE/api/v1/master/nodes/$NODE_ID/config" >/tmp/agent_verify_config.log 2>&1; then
add_result PASS "Config PUT dry-run succeeded"
else
add_result WARN "Config PUT dry-run failed: $(cat /tmp/agent_verify_config.log)"
fi
rm -f /tmp/agent_verify_config.log
fi
else
add_result WARN "Config PUT dry-run skipped (enable with --allow-config-touch)"
fi
# Result summary
echo
echo "==== Verification Summary ===="
for entry in "${RESULTS_PASS[@]}"; do
printf 'PASS: %s\n' "$entry"
done
for entry in "${RESULTS_WARN[@]}"; do
printf 'WARN: %s\n' "$entry"
done
for entry in "${RESULTS_FAIL[@]}"; do
printf 'FAIL: %s\n' "$entry"
done
if [[ ${#RESULTS_FAIL[@]} -gt 0 ]]; then
exit 1
fi
exit 0

269
src/agent/scripts/build_binary.sh Executable file
View File

@@ -0,0 +1,269 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BUILD_ROOT="$MODULE_ROOT/build"
DIST_DIR="$MODULE_ROOT/dist"
PYINSTALLER_BUILD="$BUILD_ROOT/pyinstaller"
PYINSTALLER_SPEC="$PYINSTALLER_BUILD/spec"
PYINSTALLER_WORK="$PYINSTALLER_BUILD/work"
VENV_DIR="$BUILD_ROOT/venv"
AGENT_BUILD_IMAGE="${AGENT_BUILD_IMAGE:-python:3.11-slim-bullseye}"
AGENT_BUILD_USE_DOCKER="${AGENT_BUILD_USE_DOCKER:-1}"
USED_DOCKER=0
run_host_build() {
echo "[INFO] Using host Python environment for build" >&2
rm -rf "$BUILD_ROOT" "$DIST_DIR"
mkdir -p "$PYINSTALLER_BUILD" "$DIST_DIR"
python3 -m venv --copies "$VENV_DIR"
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install .
pip install "pyinstaller==6.6.0"
pyinstaller \
--clean \
--onefile \
--name argus-agent \
--distpath "$DIST_DIR" \
--workpath "$PYINSTALLER_WORK" \
--specpath "$PYINSTALLER_SPEC" \
--add-data "$MODULE_ROOT/pyproject.toml:." \
"$MODULE_ROOT/entry.py"
chmod +x "$DIST_DIR/argus-agent"
deactivate
}
run_docker_build() {
if ! command -v docker >/dev/null 2>&1; then
echo "[ERROR] docker 命令不存在,无法在容器内构建。请安装 Docker 或设置 AGENT_BUILD_USE_DOCKER=0" >&2
exit 1
fi
USED_DOCKER=1
echo "[INFO] Building agent binary inside $AGENT_BUILD_IMAGE" >&2
local host_uid host_gid
host_uid="$(id -u)"
host_gid="$(id -g)"
docker_env=("--rm" "-v" "$MODULE_ROOT:/workspace" "-w" "/workspace" "--env" "TARGET_UID=${host_uid}" "--env" "TARGET_GID=${host_gid}")
pass_env_if_set() {
local var="$1"
local value="${!var:-}"
if [[ -n "$value" ]]; then
docker_env+=("--env" "$var=$value")
fi
}
pass_env_if_set PIP_INDEX_URL
pass_env_if_set PIP_EXTRA_INDEX_URL
pass_env_if_set PIP_TRUSTED_HOST
pass_env_if_set HTTP_PROXY
pass_env_if_set HTTPS_PROXY
pass_env_if_set NO_PROXY
pass_env_if_set http_proxy
pass_env_if_set https_proxy
pass_env_if_set no_proxy
build_script=$(cat <<'INNER'
set -euo pipefail
cd /workspace
apt-get update >/dev/null
apt-get install -y --no-install-recommends binutils >/dev/null
rm -rf /var/lib/apt/lists/*
rm -rf build dist
mkdir -p build/pyinstaller dist
python3 -m venv --copies build/venv
source build/venv/bin/activate
pip install --upgrade pip
pip install .
pip install pyinstaller==6.6.0
pyinstaller \
--clean \
--onefile \
--name argus-agent \
--distpath dist \
--workpath build/pyinstaller/work \
--specpath build/pyinstaller/spec \
--add-data /workspace/pyproject.toml:. \
entry.py
chmod +x dist/argus-agent
TARGET_UID="${TARGET_UID:-0}"
TARGET_GID="${TARGET_GID:-0}"
chown -R "$TARGET_UID:$TARGET_GID" dist build 2>/dev/null || true
python3 - <<'PY'
from pathlib import Path
from PyInstaller.archive.readers import CArchiveReader
import sys
archive = Path('dist/argus-agent')
out_dir = Path('build/compat_check')
out_dir.mkdir(parents=True, exist_ok=True)
major, minor = sys.version_info[:2]
libpython = f'libpython{major}.{minor}.so.1.0'
expected_libs = [
libpython,
'libssl.so.3',
'libcrypto.so.3',
]
reader = CArchiveReader(str(archive))
extracted = []
missing = []
for name in expected_libs:
try:
data = reader.extract(name)
except KeyError:
missing.append(name)
continue
(out_dir / name).write_bytes(data)
extracted.append(name)
(out_dir / 'manifest').write_text('\n'.join(extracted))
if extracted:
print('[INFO] Extracted libraries: ' + ', '.join(extracted))
if missing:
print('[WARN] Missing expected libraries in bundle: ' + ', '.join(missing))
PY
compat_check() {
local lib_path="$1"
if [[ ! -f "$lib_path" ]]; then
echo "[WARN] Missing $lib_path for GLIBC check"
return
fi
local max_glibc
max_glibc=$(strings -a "$lib_path" | grep -Eo 'GLIBC_[0-9]+\.[0-9]+' | sort -Vu | tail -n 1 || true)
if [[ -n "$max_glibc" ]]; then
echo "[INFO] $lib_path references up to $max_glibc"
else
echo "[INFO] $lib_path does not expose GLIBC version strings"
fi
}
compat_libs=()
if [[ -f build/compat_check/manifest ]]; then
mapfile -t compat_libs < build/compat_check/manifest
fi
if [[ ${#compat_libs[@]} -eq 0 ]]; then
echo "[WARN] No libraries captured for GLIBC inspection"
else
for lib in "${compat_libs[@]}"; do
compat_check "build/compat_check/$lib"
done
fi
deactivate
INNER
)
if ! docker run "${docker_env[@]}" "$AGENT_BUILD_IMAGE" bash -lc "$build_script"; then
echo "[ERROR] Docker 构建失败,请检查 Docker 权限或设置 AGENT_BUILD_USE_DOCKER=0 在兼容主机上构建" >&2
exit 1
fi
}
if [[ "$AGENT_BUILD_USE_DOCKER" == "1" ]]; then
run_docker_build
else
run_host_build
fi
if [[ ! -f "$DIST_DIR/argus-agent" ]]; then
echo "[ERROR] Agent binary was not produced" >&2
exit 1
fi
if [[ "$USED_DOCKER" != "1" ]]; then
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
echo "[WARN] PyInstaller virtualenv missing at $VENV_DIR; skipping compatibility check" >&2
else
COMPAT_DIR="$BUILD_ROOT/compat_check"
rm -rf "$COMPAT_DIR"
mkdir -p "$COMPAT_DIR"
EXTRACT_SCRIPT=$(cat <<'PY'
from pathlib import Path
from PyInstaller.archive.readers import CArchiveReader
import sys
archive = Path('dist/argus-agent')
out_dir = Path('build/compat_check')
out_dir.mkdir(parents=True, exist_ok=True)
major, minor = sys.version_info[:2]
libpython = f'libpython{major}.{minor}.so.1.0'
expected_libs = [
libpython,
'libssl.so.3',
'libcrypto.so.3',
]
reader = CArchiveReader(str(archive))
extracted = []
missing = []
for name in expected_libs:
try:
data = reader.extract(name)
except KeyError:
missing.append(name)
continue
(out_dir / name).write_bytes(data)
extracted.append(name)
(out_dir / 'manifest').write_text('\n'.join(extracted))
if extracted:
print('[INFO] Extracted libraries: ' + ', '.join(extracted))
if missing:
print('[WARN] Missing expected libraries in bundle: ' + ', '.join(missing))
PY
)
"$VENV_DIR/bin/python" - <<PY
$EXTRACT_SCRIPT
PY
compat_libs=()
if [[ -f "$COMPAT_DIR/manifest" ]]; then
mapfile -t compat_libs < "$COMPAT_DIR/manifest"
fi
check_glibc_version() {
local lib_path="$1"
if [[ ! -f "$lib_path" ]]; then
echo "[WARN] Skipping GLIBC check; file not found: $lib_path" >&2
return
fi
if command -v strings >/dev/null 2>&1; then
local max_glibc
max_glibc=$(strings -a "$lib_path" | grep -Eo 'GLIBC_[0-9]+\.[0-9]+' | sort -Vu | tail -n 1 || true)
if [[ -n "$max_glibc" ]]; then
echo "[INFO] $lib_path references up to $max_glibc"
else
echo "[INFO] $lib_path does not expose GLIBC version strings"
fi
else
echo "[WARN] strings command unavailable; cannot inspect $lib_path" >&2
fi
}
if [[ ${#compat_libs[@]} -eq 0 ]]; then
echo "[WARN] No libraries captured for GLIBC inspection" >&2
else
for lib in "${compat_libs[@]}"; do
check_glibc_version "$COMPAT_DIR/$lib"
done
fi
fi
else
echo "[INFO] Compatibility check executed inside container"
fi
echo "[INFO] Agent binary generated at $DIST_DIR/argus-agent"

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 [--intranet] [--tag <image_tag>]" >&2
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
IMAGE_TAG="${IMAGE_TAG:-argus-agent:dev}"
BUILD_ARGS=()
while [[ "$#" -gt 0 ]]; do
case "$1" in
--intranet)
INTRANET_INDEX="${INTRANET_INDEX:-https://pypi.tuna.tsinghua.edu.cn/simple}"
BUILD_ARGS+=("--build-arg" "PIP_INDEX_URL=${INTRANET_INDEX}")
shift
;;
--tag)
[[ $# -ge 2 ]] || { usage; exit 1; }
IMAGE_TAG="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
echo "[INFO] Building image $IMAGE_TAG"
docker build "${BUILD_ARGS[@]}" -t "$IMAGE_TAG" "$PROJECT_ROOT"
echo "[OK] Image $IMAGE_TAG built"

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 [--file <tar_path>]" >&2
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEFAULT_INPUT="$PROJECT_ROOT/images/argus-agent-dev.tar"
IMAGE_TAR="$DEFAULT_INPUT"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--file)
[[ $# -ge 2 ]] || { usage; exit 1; }
IMAGE_TAR="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
if [[ ! -f "$IMAGE_TAR" ]]; then
echo "[ERROR] Image tarball not found: $IMAGE_TAR" >&2
exit 1
fi
echo "[INFO] Loading image from $IMAGE_TAR"
docker image load -i "$IMAGE_TAR"
echo "[OK] Image loaded"

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 [--tag <image_tag>] [--output <tar_path>]" >&2
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEFAULT_OUTPUT="$PROJECT_ROOT/images/argus-agent-dev.tar"
IMAGE_TAG="${IMAGE_TAG:-argus-agent:dev}"
OUTPUT_PATH="$DEFAULT_OUTPUT"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--tag)
[[ $# -ge 2 ]] || { usage; exit 1; }
IMAGE_TAG="$2"
shift 2
;;
--output)
[[ $# -ge 2 ]] || { usage; exit 1; }
OUTPUT_PATH="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
mkdir -p "$(dirname "$OUTPUT_PATH")"
echo "[INFO] Saving image $IMAGE_TAG to $OUTPUT_PATH"
docker image save "$IMAGE_TAG" -o "$OUTPUT_PATH"
echo "[OK] Image saved"

View File

@@ -1,26 +1,64 @@
services:
bind:
image: ${BIND_IMAGE_TAG:-argus-bind9:latest}
container_name: argus-bind-agent-e2e
volumes:
- ./private:/private
networks:
default:
ipv4_address: 172.28.0.2
environment:
- "ARGUS_BUILD_UID=${ARGUS_BUILD_UID:-2133}"
- "ARGUS_BUILD_GID=${ARGUS_BUILD_GID:-2015}"
restart: always
master:
image: argus-master:dev
image: argus-master:latest
container_name: argus-master-agent-e2e
depends_on:
- bind
environment:
- OFFLINE_THRESHOLD_SECONDS=6
- ONLINE_THRESHOLD_SECONDS=2
- SCHEDULER_INTERVAL_SECONDS=1
- "ARGUS_BUILD_UID=${ARGUS_BUILD_UID:-2133}"
- "ARGUS_BUILD_GID=${ARGUS_BUILD_GID:-2015}"
ports:
- "32300:3000"
volumes:
- ./private/argus/master:/private/argus/master
- ./private/argus/metric/prometheus:/private/argus/metric/prometheus
- ./private/argus/etc:/private/argus/etc
networks:
default:
ipv4_address: 172.28.0.10
restart: always
agent:
image: argus-agent:dev
image: ubuntu:22.04
container_name: argus-agent-e2e
hostname: dev-e2euser-e2einst-pod-0
depends_on:
- master
- bind
environment:
- MASTER_ENDPOINT=http://master.argus.com:3000
- REPORT_INTERVAL_SECONDS=2
- "ARGUS_BUILD_UID=${ARGUS_BUILD_UID:-2133}"
- "ARGUS_BUILD_GID=${ARGUS_BUILD_GID:-2015}"
volumes:
- ./private/argus/agent/dev-e2euser-e2einst-pod-0:/private/argus/agent/dev-e2euser-e2einst-pod-0
- ./private/argus/agent/health/dev-e2euser-e2einst-pod-0:/private/argus/agent/health/dev-e2euser-e2einst-pod-0
- ./private/argus/agent/dev-e2euser-e2einst-pod-0/health:/private/argus/agent/dev-e2euser-e2einst-pod-0/health
- ./private/argus/etc:/private/argus/etc
- ../dist/argus-agent:/usr/local/bin/argus-agent:ro
- ./scripts/agent_entrypoint.sh:/usr/local/bin/agent-entrypoint.sh:ro
- ../scripts/agent_deployment_verify.sh:/usr/local/bin/agent_deployment_verify.sh:ro
entrypoint:
- /usr/local/bin/agent-entrypoint.sh
networks:
default:
ipv4_address: 172.28.0.20
restart: always
networks:
default:

View File

@@ -7,6 +7,7 @@ SCRIPTS=(
"02_up.sh"
"03_wait_and_assert_registration.sh"
"04_write_health_files.sh"
"08_verify_agent.sh"
"05_assert_status_on_master.sh"
"06_restart_agent_and_reregister.sh"
"07_down.sh"

View File

@@ -5,40 +5,59 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
AGENT_ROOT="$(cd "$TEST_ROOT/.." && pwd)"
MASTER_ROOT="$(cd "$AGENT_ROOT/../master" && pwd)"
REPO_ROOT="$(cd "$AGENT_ROOT/../.." && pwd)"
PRIVATE_ROOT="$TEST_ROOT/private"
TMP_ROOT="$TEST_ROOT/tmp"
AGENT_HOSTNAME="dev-e2euser-e2einst-pod-0"
AGENT_CONFIG_DIR="$PRIVATE_ROOT/argus/agent/$AGENT_HOSTNAME"
AGENT_HEALTH_DIR="$PRIVATE_ROOT/argus/agent/health/$AGENT_HOSTNAME"
AGENT_HEALTH_DIR="$PRIVATE_ROOT/argus/agent/$AGENT_HOSTNAME/health"
MASTER_PRIVATE_DIR="$PRIVATE_ROOT/argus/master"
METRIC_PRIVATE_DIR="$PRIVATE_ROOT/argus/metric/prometheus"
DNS_DIR="$PRIVATE_ROOT/argus/etc"
BIND_IMAGE_TAG="${BIND_IMAGE_TAG:-argus-bind9:latest}"
BIND_ROOT="$(cd "$MASTER_ROOT/../bind" && pwd)"
ensure_image() {
local image="$1"
if ! docker image inspect "$image" >/dev/null 2>&1; then
echo "[ERROR] Docker image '$image' 未找到,请先运行统一构建脚本 (例如 ./build/build_images.sh) 生成所需镜像" >&2
exit 1
fi
}
mkdir -p "$AGENT_CONFIG_DIR"
mkdir -p "$AGENT_HEALTH_DIR"
mkdir -p "$MASTER_PRIVATE_DIR"
mkdir -p "$METRIC_PRIVATE_DIR"
mkdir -p "$TMP_ROOT"
cat > "$AGENT_CONFIG_DIR/config" <<CONFIG
HOSTNAME=$AGENT_HOSTNAME
NODE_FILE=/private/argus/agent/$AGENT_HOSTNAME/node.json
VERSION=1.1.0
MASTER_ENDPOINT=http://master:3000
REPORT_INTERVAL_SECONDS=2
SUBMODULE_HEALTH_FILE_DIR=/private/argus/agent/health/{hostname}/
GPU_NUMBER=
IP_OVERRIDE=
CONFIG
mkdir -p "$DNS_DIR"
touch "$AGENT_HEALTH_DIR/.keep"
pushd "$MASTER_ROOT" >/dev/null
./scripts/build_images.sh --tag argus-master:dev
popd >/dev/null
# 中文提示:准备 bind 模块提供的 update-dns.sh,模拟生产下发
if [[ -f "$BIND_ROOT/build/update-dns.sh" ]]; then
cp "$BIND_ROOT/build/update-dns.sh" "$DNS_DIR/update-dns.sh"
chmod +x "$DNS_DIR/update-dns.sh"
else
echo "[WARN] bind update script missing at $BIND_ROOT/build/update-dns.sh"
fi
ensure_image "argus-master:latest"
ensure_image "$BIND_IMAGE_TAG"
AGENT_BINARY="$AGENT_ROOT/dist/argus-agent"
pushd "$AGENT_ROOT" >/dev/null
./scripts/build_images.sh --tag argus-agent:dev
./scripts/build_binary.sh
popd >/dev/null
if [[ ! -x "$AGENT_BINARY" ]]; then
echo "[ERROR] Agent binary not found at $AGENT_BINARY" >&2
exit 1
fi
echo "$AGENT_BINARY" > "$TMP_ROOT/agent_binary_path"
echo "$BIND_IMAGE_TAG" > "$TMP_ROOT/bind_image_tag"
echo "[INFO] Agent E2E bootstrap complete"

View File

@@ -3,6 +3,35 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$TEST_ROOT/../../.." && pwd)"
TMP_ROOT="$TEST_ROOT/tmp"
ENV_FILE="$TEST_ROOT/.env"
source "$REPO_ROOT/scripts/common/build_user.sh"
load_build_user
export ARGUS_BUILD_UID ARGUS_BUILD_GID
cat > "$ENV_FILE" <<EOF
ARGUS_BUILD_UID=$ARGUS_BUILD_UID
ARGUS_BUILD_GID=$ARGUS_BUILD_GID
EOF
if [[ ! -f "$TMP_ROOT/agent_binary_path" ]]; then
echo "[ERROR] Agent binary path missing; run 01_bootstrap.sh first" >&2
exit 1
fi
AGENT_BINARY="$(cat "$TMP_ROOT/agent_binary_path")"
if [[ ! -x "$AGENT_BINARY" ]]; then
echo "[ERROR] Agent binary not executable: $AGENT_BINARY" >&2
exit 1
fi
BIND_IMAGE_TAG_VALUE="argus-bind9:latest"
if [[ -f "$TMP_ROOT/bind_image_tag" ]]; then
BIND_IMAGE_TAG_VALUE="$(cat "$TMP_ROOT/bind_image_tag")"
fi
compose() {
if docker compose version >/dev/null 2>&1; then
@@ -12,13 +41,13 @@ compose() {
fi
}
docker container rm -f argus-agent-e2e >/dev/null 2>&1 || true
docker container rm -f argus-agent-e2e argus-master-agent-e2e argus-bind-agent-e2e >/dev/null 2>&1 || true
docker network rm tests_default >/dev/null 2>&1 || true
pushd "$TEST_ROOT" >/dev/null
compose down --remove-orphans || true
compose up -d
BIND_IMAGE_TAG="$BIND_IMAGE_TAG_VALUE" compose up -d
popd >/dev/null
echo "[INFO] Master+Agent stack started"

View File

@@ -3,7 +3,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
HEALTH_DIR="$TEST_ROOT/private/argus/agent/health/dev-e2euser-e2einst-pod-0"
HEALTH_DIR="$TEST_ROOT/private/argus/agent/dev-e2euser-e2einst-pod-0/health"
cat > "$HEALTH_DIR/log-fluentbit.json" <<JSON
{

View File

@@ -9,6 +9,40 @@ NODE_ID="$(cat "$TMP_ROOT/node_id")"
AGENT_HOSTNAME="dev-e2euser-e2einst-pod-0"
NETWORK_NAME="tests_default"
NEW_AGENT_IP="172.28.0.200"
ENTRYPOINT_SCRIPT="$SCRIPT_DIR/agent_entrypoint.sh"
ENV_FILE="$TEST_ROOT/.env"
# 中文提示:重启场景也需要同样的入口脚本,确保 DNS 注册逻辑一致
if [[ ! -f "$ENTRYPOINT_SCRIPT" ]]; then
echo "[ERROR] agent entrypoint script missing at $ENTRYPOINT_SCRIPT" >&2
exit 1
fi
if [[ ! -f "$TMP_ROOT/agent_binary_path" ]]; then
echo "[ERROR] Agent binary path missing; rerun bootstrap" >&2
exit 1
fi
AGENT_BINARY="$(cat "$TMP_ROOT/agent_binary_path")"
if [[ ! -x "$AGENT_BINARY" ]]; then
echo "[ERROR] Agent binary not executable: $AGENT_BINARY" >&2
exit 1
fi
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
else
REPO_ROOT="$(cd "$TEST_ROOT/../../.." && pwd)"
# shellcheck disable=SC1090
source "$REPO_ROOT/scripts/common/build_user.sh"
load_build_user
fi
AGENT_UID="${ARGUS_BUILD_UID:-2133}"
AGENT_GID="${ARGUS_BUILD_GID:-2015}"
compose() {
if docker compose version >/dev/null 2>&1; then
@@ -47,7 +81,7 @@ popd >/dev/null
docker container rm -f argus-agent-e2e >/dev/null 2>&1 || true
AGENT_DIR="$TEST_ROOT/private/argus/agent/$AGENT_HOSTNAME"
HEALTH_DIR="$TEST_ROOT/private/argus/agent/health/$AGENT_HOSTNAME"
HEALTH_DIR="$TEST_ROOT/private/argus/agent/$AGENT_HOSTNAME/health"
# 先以 sleep 方式启动容器,确保我们掌握注册时的网络状态
if ! docker run -d \
@@ -56,19 +90,20 @@ if ! docker run -d \
--network "$NETWORK_NAME" \
--ip "$NEW_AGENT_IP" \
-v "$AGENT_DIR:/private/argus/agent/$AGENT_HOSTNAME" \
-v "$HEALTH_DIR:/private/argus/agent/health/$AGENT_HOSTNAME" \
argus-agent:dev \
sleep 300 >/dev/null; then
-v "$HEALTH_DIR:/private/argus/agent/$AGENT_HOSTNAME/health" \
-v "$TEST_ROOT/private/argus/etc:/private/argus/etc" \
-v "$AGENT_BINARY:/usr/local/bin/argus-agent:ro" \
-v "$ENTRYPOINT_SCRIPT:/usr/local/bin/agent-entrypoint.sh:ro" \
-e MASTER_ENDPOINT=http://master.argus.com:3000 \
-e REPORT_INTERVAL_SECONDS=2 \
-e ARGUS_BUILD_UID="$AGENT_UID" \
-e ARGUS_BUILD_GID="$AGENT_GID" \
--entrypoint /usr/local/bin/agent-entrypoint.sh \
ubuntu:22.04 >/dev/null; then
echo "[ERROR] Failed to start agent container with custom IP" >&2
exit 1
fi
# 在容器内启动真实 agent 进程
if ! docker exec -d argus-agent-e2e python -m app.main; then
echo "[ERROR] Failed to spawn agent process inside container" >&2
exit 1
fi
success=false
detail_file="$TMP_ROOT/post_restart.json"
for _ in {1..20}; do

View File

@@ -3,6 +3,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ENV_FILE="$TEST_ROOT/.env"
compose() {
if docker compose version >/dev/null 2>&1; then
@@ -18,7 +19,18 @@ pushd "$TEST_ROOT" >/dev/null
compose down --remove-orphans
popd >/dev/null
if [[ -d "$TEST_ROOT/private" ]]; then
docker run --rm \
-v "$TEST_ROOT/private:/target" \
ubuntu:24.04 \
chown -R "$(id -u):$(id -g)" /target >/dev/null 2>&1 || true
rm -rf "$TEST_ROOT/private"
fi
rm -rf "$TEST_ROOT/tmp"
if [[ -f "$ENV_FILE" ]]; then
rm -f "$ENV_FILE"
fi
echo "[INFO] Agent E2E environment cleaned up"

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
VERIFY_SCRIPT="$(cd "$TEST_ROOT/.." && pwd)/scripts/agent_deployment_verify.sh"
if ! docker ps --format '{{.Names}}' | grep -q '^argus-agent-e2e$'; then
echo "[WARN] agent container not running; skip verification"
exit 0
fi
if docker exec -i argus-agent-e2e bash -lc 'command -v curl >/dev/null && command -v jq >/dev/null'; then
echo "[INFO] curl/jq already installed in agent container"
else
echo "[INFO] Installing curl/jq in agent container"
docker exec -i argus-agent-e2e bash -lc 'apt-get update >/dev/null 2>&1 && apt-get install -y curl jq >/dev/null 2>&1' || true
fi
if docker exec -i argus-agent-e2e bash -lc 'command -v /usr/local/bin/agent_deployment_verify.sh >/dev/null'; then
docker exec -i argus-agent-e2e /usr/local/bin/agent_deployment_verify.sh
elif [[ -x "$VERIFY_SCRIPT" ]]; then
docker exec -i argus-agent-e2e "$VERIFY_SCRIPT"
else
echo "[WARN] agent_deployment_verify.sh not found"
fi

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
LOG_PREFIX="[AGENT-ENTRYPOINT]"
DNS_SCRIPT="/private/argus/etc/update-dns.sh"
DNS_CONF="/private/argus/etc/dns.conf"
TARGET_DOMAIN="master.argus.com"
AGENT_UID="${ARGUS_BUILD_UID:-2133}"
AGENT_GID="${ARGUS_BUILD_GID:-2015}"
AGENT_HOSTNAME="${HOSTNAME:-unknown}"
AGENT_DATA_DIR="/private/argus/agent/${AGENT_HOSTNAME}"
AGENT_HEALTH_DIR="${AGENT_DATA_DIR}/health"
RUNTIME_GROUP="argusagent"
RUNTIME_USER="argusagent"
log() {
echo "${LOG_PREFIX} $*"
}
mkdir -p "$AGENT_DATA_DIR" "$AGENT_HEALTH_DIR"
chown -R "$AGENT_UID:$AGENT_GID" "$AGENT_DATA_DIR" "$AGENT_HEALTH_DIR" 2>/dev/null || true
chown -R "$AGENT_UID:$AGENT_GID" "/private/argus/etc" 2>/dev/null || true
if ! getent group "$AGENT_GID" >/dev/null 2>&1; then
groupadd -g "$AGENT_GID" "$RUNTIME_GROUP"
else
RUNTIME_GROUP="$(getent group "$AGENT_GID" | cut -d: -f1)"
fi
if ! getent passwd "$AGENT_UID" >/dev/null 2>&1; then
useradd -u "$AGENT_UID" -g "$AGENT_GID" -M -s /bin/bash "$RUNTIME_USER"
else
RUNTIME_USER="$(getent passwd "$AGENT_UID" | cut -d: -f1)"
fi
log "运行用户: $RUNTIME_USER ($AGENT_UID:$AGENT_GID)"
# 中文提示:等待 bind 下发的 update-dns.sh 脚本
for _ in {1..30}; do
if [[ -x "$DNS_SCRIPT" ]]; then
break
fi
log "等待 update-dns.sh 准备就绪..."
sleep 1
done
if [[ -x "$DNS_SCRIPT" ]]; then
log "执行 update-dns.sh 更新容器 DNS"
while true; do
if "$DNS_SCRIPT"; then
log "update-dns.sh 执行成功"
break
fi
log "update-dns.sh 执行失败,3 秒后重试"
sleep 3
done
else
log "未获取到 update-dns.sh,使用镜像默认 DNS"
fi
# 中文提示:记录当前 dns.conf 内容,便于排查
if [[ -f "$DNS_CONF" ]]; then
log "dns.conf 内容: $(tr '\n' ' ' < "$DNS_CONF")"
else
log "dns.conf 暂未生成"
fi
# 中文提示:尝试解析 master 域名,失败不阻塞但会打日志
for _ in {1..30}; do
if getent hosts "$TARGET_DOMAIN" >/dev/null 2>&1; then
MASTER_IP=$(getent hosts "$TARGET_DOMAIN" | awk '{print $1}' | head -n 1)
log "master.argus.com 解析成功: $MASTER_IP"
break
fi
sleep 1
done
log "启动 argus-agent"
exec su -s /bin/bash -c /usr/local/bin/argus-agent "$RUNTIME_USER"

View File

@@ -6,6 +6,11 @@ ENV TZ=Asia/Shanghai
# 设置构建参数
ARG USE_INTRANET=false
ARG ARGUS_BUILD_UID=2133
ARG ARGUS_BUILD_GID=2015
ENV ARGUS_BUILD_UID=${ARGUS_BUILD_UID} \
ARGUS_BUILD_GID=${ARGUS_BUILD_GID}
# 配置内网 apt 源 (如果指定了内网选项)
RUN if [ "$USE_INTRANET" = "true" ]; then \
@@ -29,6 +34,24 @@ RUN apt-get update && \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 调整 bind 用户与用户组 ID 以匹配宿主机配置
RUN set -eux; \
current_gid="$(getent group bind | awk -F: '{print $3}')"; \
if [ -z "$current_gid" ]; then \
groupadd -g "${ARGUS_BUILD_GID}" bind; \
elif [ "$current_gid" != "${ARGUS_BUILD_GID}" ]; then \
groupmod -g "${ARGUS_BUILD_GID}" bind; \
fi; \
if id bind >/dev/null 2>&1; then \
current_uid="$(id -u bind)"; \
if [ "$current_uid" != "${ARGUS_BUILD_UID}" ]; then \
usermod -u "${ARGUS_BUILD_UID}" bind; \
fi; \
else \
useradd -m -u "${ARGUS_BUILD_UID}" -g "${ARGUS_BUILD_GID}" bind; \
fi; \
chown -R "${ARGUS_BUILD_UID}:${ARGUS_BUILD_GID}" /var/cache/bind /var/lib/bind
# 配置部署时使用的apt源
RUN if [ "$USE_INTRANET" = "true" ]; then \
echo "deb [trusted=yes] https://10.92.132.52/mirrors/ubuntu2204/ jammy main" > /etc/apt/sources.list; \

View File

@@ -9,6 +9,9 @@ SLEEP_SECONDS=10
RELOAD_SCRIPT="/usr/local/bin/reload-bind9.sh" # 这里放你已有脚本的路径
mkdir -p "$(dirname "$LOCKFILE")" "$BACKUP_DIR"
BACKUP_UID="${ARGUS_BUILD_UID:-2133}"
BACKUP_GID="${ARGUS_BUILD_GID:-2015}"
chown -R "$BACKUP_UID:$BACKUP_GID" "$BACKUP_DIR" 2>/dev/null || true
is_ipv4() {
local ip="$1"
@@ -33,6 +36,7 @@ upsert_record() {
local changed=0
cp -a "$ZONE_DB" "$BACKUP_DIR/db.argus.com.$ts.bak"
chown "$BACKUP_UID:$BACKUP_GID" "$BACKUP_DIR/db.argus.com.$ts.bak" 2>/dev/null || true
local cur_ip
cur_ip="$(get_current_ip "$name" || true)"
@@ -61,7 +65,10 @@ upsert_record() {
echo "[SKIP] ${name} unchanged (${new_ip})"
fi
return $changed
if [[ $changed -eq 1 ]]; then
return 0
fi
return 1
}
while true; do
@@ -97,4 +104,3 @@ while true; do
sleep "$SLEEP_SECONDS"
done

View File

@@ -6,6 +6,8 @@ chmod 777 /private 2>/dev/null || true
# Create persistent directories for BIND9 configs and DNS sync
mkdir -p /private/argus/bind
mkdir -p /private/argus/etc
chown bind:bind /private/argus 2>/dev/null || true
chown -R bind:bind /private/argus/bind /private/argus/etc
# Copy configuration files to persistent storage if they don't exist
if [ ! -f /private/argus/bind/named.conf.local ]; then

View File

@@ -3,8 +3,8 @@ services:
image: argus-bind9:latest
container_name: argus-bind9-test
ports:
- "53:53/tcp"
- "53:53/udp"
- "${HOST_DNS_PORT:-1053}:53/tcp"
- "${HOST_DNS_PORT:-1053}:53/udp"
volumes:
- ./private:/private
restart: unless-stopped

View File

@@ -7,6 +7,9 @@
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOST_DNS_PORT="${HOST_DNS_PORT:-1053}"
export HOST_DNS_PORT
echo "=========================================="
echo "BIND9 DNS Server End-to-End Test Suite"

View File

@@ -7,13 +7,17 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="$(dirname "$SCRIPT_DIR")"
HOST_DNS_PORT="${HOST_DNS_PORT:-1053}"
export HOST_DNS_PORT
cd "$TEST_DIR"
echo "Starting BIND9 test container..."
# Ensure private directory exists with proper permissions
mkdir -p private
mkdir -p private/argus/bind
mkdir -p private/argus/etc
chmod 777 private
# Start the container
@@ -35,4 +39,4 @@ fi
echo ""
echo "BIND9 test environment is ready!"
echo "DNS server listening on localhost:53"
echo "DNS server listening on localhost:${HOST_DNS_PORT}"

View File

@@ -5,7 +5,10 @@
set -e
HOST_DNS_PORT="${HOST_DNS_PORT:-1053}"
echo "Testing DNS resolution with dig..."
echo "Using DNS server localhost:${HOST_DNS_PORT}"
# Function to test DNS query
test_dns_query() {
@@ -19,7 +22,7 @@ test_dns_query() {
echo "Expected IP: $expected_ip"
# Perform dig query
result=$(dig @localhost $hostname.argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
result=$(dig @localhost -p "$HOST_DNS_PORT" "$hostname".argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
if [ "$result" = "QUERY_FAILED" ]; then
echo "✗ DNS query failed"

View File

@@ -6,10 +6,13 @@
set -e
HOST_DNS_PORT="${HOST_DNS_PORT:-1053}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="$(dirname "$SCRIPT_DIR")"
echo "=== DNS Auto-Sync Functionality Test ==="
echo "Using DNS server localhost:${HOST_DNS_PORT}"
# Check if container is running
if ! docker compose ps | grep -q "Up"; then
@@ -36,7 +39,7 @@ test_dns_query() {
# Wait a moment for DNS cache
sleep 2
result=$(dig @localhost $hostname.argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
result=$(dig @localhost -p "$HOST_DNS_PORT" "$hostname".argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
if [ "$result" = "$expected_ip" ]; then
echo "✓ $result"
@@ -90,7 +93,7 @@ echo ""
echo "Step 2: Testing initial DNS configuration..."
# Get current IP for web.argus.com (may have been changed by previous tests)
current_web_ip=$(dig @localhost web.argus.com A +short 2>/dev/null || echo "UNKNOWN")
current_web_ip=$(dig @localhost -p "$HOST_DNS_PORT" web.argus.com A +short 2>/dev/null || echo "UNKNOWN")
echo "Current web.argus.com IP: $current_web_ip"
# Test that DNS is working (regardless of specific IP)
@@ -185,7 +188,7 @@ docker compose exec bind9 bash -c 'echo "this is not an IP address" > /private/a
wait_for_sync
# Verify invalid record was not added (should fail to resolve)
result=$(dig @localhost invalid.argus.com A +short 2>/dev/null || echo "NO_RESULT")
result=$(dig @localhost -p "$HOST_DNS_PORT" invalid.argus.com A +short 2>/dev/null || echo "NO_RESULT")
if [ "$result" = "NO_RESULT" ] || [ -z "$result" ]; then
echo "✓ Invalid IP correctly ignored"
else

View File

@@ -5,10 +5,13 @@
set -e
HOST_DNS_PORT="${HOST_DNS_PORT:-1053}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="$(dirname "$SCRIPT_DIR")"
echo "=== DNS Configuration Reload Test ==="
echo "Using DNS server localhost:${HOST_DNS_PORT}"
# Check if container is running
if ! docker compose ps | grep -q "Up"; then
@@ -32,7 +35,7 @@ test_dns_query() {
echo "Testing: $description"
echo "Query: $hostname.argus.com -> Expected: $expected_ip"
result=$(dig @localhost $hostname.argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
result=$(dig @localhost -p "$HOST_DNS_PORT" "$hostname".argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
if [ "$result" = "$expected_ip" ]; then
echo "✓ $result"

View File

@@ -5,10 +5,13 @@
set -e
HOST_DNS_PORT="${HOST_DNS_PORT:-1053}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="$(dirname "$SCRIPT_DIR")"
echo "=== Configuration Persistence Test ==="
echo "Using DNS server localhost:${HOST_DNS_PORT}"
# Check if dig is available
if ! command -v dig &> /dev/null; then
@@ -25,7 +28,7 @@ test_dns_query() {
echo "Testing: $description"
echo "Query: $hostname.argus.com -> Expected: $expected_ip"
result=$(dig @localhost $hostname.argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
result=$(dig @localhost -p "$HOST_DNS_PORT" "$hostname".argus.com A +short 2>/dev/null || echo "QUERY_FAILED")
if [ "$result" = "$expected_ip" ]; then
echo "✓ $result"

View File

@@ -7,6 +7,9 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="$(dirname "$SCRIPT_DIR")"
HOST_DNS_PORT="${HOST_DNS_PORT:-1053}"
export HOST_DNS_PORT
# Parse command line arguments
FULL_CLEANUP=true

View File

@@ -3,10 +3,29 @@ FROM docker.elastic.co/elasticsearch/elasticsearch:8.13.4
# 切换到 root 用户进行系统级安装
USER root
# 修改elasticsearch用户的UID和GID
RUN usermod -u 2133 elasticsearch && \
groupmod -g 2015 elasticsearch && \
chown -R elasticsearch:elasticsearch /usr/share/elasticsearch
ARG ARGUS_BUILD_UID=2133
ARG ARGUS_BUILD_GID=2015
ENV ARGUS_BUILD_UID=${ARGUS_BUILD_UID} \
ARGUS_BUILD_GID=${ARGUS_BUILD_GID}
# 调整 elasticsearch 用户与用户组 ID 以匹配宿主机配置
RUN set -eux; \
current_gid="$(getent group elasticsearch | awk -F: '{print $3}')"; \
if [ -z "$current_gid" ]; then \
groupadd -g "${ARGUS_BUILD_GID}" elasticsearch; \
elif [ "$current_gid" != "${ARGUS_BUILD_GID}" ]; then \
groupmod -g "${ARGUS_BUILD_GID}" elasticsearch; \
fi; \
if id elasticsearch >/dev/null 2>&1; then \
current_uid="$(id -u elasticsearch)"; \
if [ "$current_uid" != "${ARGUS_BUILD_UID}" ]; then \
usermod -u "${ARGUS_BUILD_UID}" elasticsearch; \
fi; \
else \
useradd -m -u "${ARGUS_BUILD_UID}" -g "${ARGUS_BUILD_GID}" elasticsearch; \
fi; \
chown -R "${ARGUS_BUILD_UID}:${ARGUS_BUILD_GID}" /usr/share/elasticsearch
# 设置构建参数
ARG USE_INTRANET=false

View File

@@ -3,10 +3,29 @@ FROM docker.elastic.co/kibana/kibana:8.13.4
# 切换到 root 用户进行系统级安装
USER root
# 修改kibana用户的UID和GID
RUN usermod -u 2133 kibana && \
groupmod -g 2015 kibana && \
chown -R kibana:kibana /usr/share/kibana
ARG ARGUS_BUILD_UID=2133
ARG ARGUS_BUILD_GID=2015
ENV ARGUS_BUILD_UID=${ARGUS_BUILD_UID} \
ARGUS_BUILD_GID=${ARGUS_BUILD_GID}
# 调整 kibana 用户与用户组 ID 以匹配宿主机配置
RUN set -eux; \
current_gid="$(getent group kibana | awk -F: '{print $3}')"; \
if [ -z "$current_gid" ]; then \
groupadd -g "${ARGUS_BUILD_GID}" kibana; \
elif [ "$current_gid" != "${ARGUS_BUILD_GID}" ]; then \
groupmod -g "${ARGUS_BUILD_GID}" kibana; \
fi; \
if id kibana >/dev/null 2>&1; then \
current_uid="$(id -u kibana)"; \
if [ "$current_uid" != "${ARGUS_BUILD_UID}" ]; then \
usermod -u "${ARGUS_BUILD_UID}" kibana; \
fi; \
else \
useradd -m -u "${ARGUS_BUILD_UID}" -g "${ARGUS_BUILD_GID}" kibana; \
fi; \
chown -R "${ARGUS_BUILD_UID}:${ARGUS_BUILD_GID}" /usr/share/kibana
# 设置构建参数
ARG USE_INTRANET=false

View File

@@ -17,6 +17,7 @@ services:
interval: 10s
timeout: 5s
retries: 30
restart: always
kibana:
build:
@@ -73,13 +74,11 @@ services:
interval: 15s
timeout: 10s
retries: 30
restart: always
bind9:
image: argus-bind9:latest
ports:
- "53:53/tcp"
- "53:53/udp"
volumes:
- ./private/argus:/private/argus/
restart: unless-stopped
restart: always

View File

@@ -1,6 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../" && pwd)"
project_root="$(cd "$root/../../.." && pwd)"
source "$project_root/scripts/common/build_user.sh"
load_build_user
# 创建新的private目录结构 (基于argus目录结构)
echo "[INFO] Creating private directory structure for supervisor-based containers..."
@@ -11,9 +15,9 @@ mkdir -p "$root/private/argus/etc/"
# 设置数据目录权限(ES 和 Kibana 容器都使用 UID 1000)
echo "[INFO] Setting permissions for data directories..."
sudo chown -R 2133:2015 "$root/private/argus/log/elasticsearch" 2>/dev/null || true
sudo chown -R 2133:2015 "$root/private/argus/log/kibana" 2>/dev/null || true
sudo chown -R 2133:2015 "$root/private/argus/etc" 2>/dev/null || true
chown -R "${ARGUS_BUILD_UID}:${ARGUS_BUILD_GID}" "$root/private/argus/log/elasticsearch" 2>/dev/null || true
chown -R "${ARGUS_BUILD_UID}:${ARGUS_BUILD_GID}" "$root/private/argus/log/kibana" 2>/dev/null || true
chown -R "${ARGUS_BUILD_UID}:${ARGUS_BUILD_GID}" "$root/private/argus/etc" 2>/dev/null || true
echo "[INFO] Supervisor-based containers will manage their own scripts and configurations"

View File

@@ -4,8 +4,22 @@ set -euo pipefail
# 获取fluent-bit-host01容器名称
container_name="logging-mvp-fluent-bit-host01-1"
# 检查容器是否存在并运行
if ! docker ps | grep -q "$container_name"; then
wait_for_container() {
local name="$1"
local attempts=30
local delay=5
local i
for ((i = 1; i <= attempts; i++)); do
if docker ps --format '{{.Names}}' | grep -Fx "$name" >/dev/null; then
return 0
fi
echo "[INFO] 等待容器 $name 启动中... ($i/$attempts)"
sleep "$delay"
done
return 1
}
if ! wait_for_container "$container_name"; then
echo "[ERROR] Fluent Bit容器 $container_name 未运行"
exit 1
fi

View File

@@ -4,8 +4,22 @@ set -euo pipefail
# 获取fluent-bit-host02容器名称
container_name="logging-mvp-fluent-bit-host02-1"
# 检查容器是否存在并运行
if ! docker ps | grep -q "$container_name"; then
wait_for_container() {
local name="$1"
local attempts=30
local delay=5
local i
for ((i = 1; i <= attempts; i++)); do
if docker ps --format '{{.Names}}' | grep -Fx "$name" >/dev/null; then
return 0
fi
echo "[INFO] 等待容器 $name 启动中... ($i/$attempts)"
sleep "$delay"
done
return 1
}
if ! wait_for_container "$container_name"; then
echo "[ERROR] Fluent Bit容器 $container_name 未运行"
exit 1
fi

View File

@@ -1,7 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
# ES endpoint and wait strategy
ES="${ES:-http://localhost:9200}"
es_wait_attempts="${ES_WAIT_ATTEMPTS:-60}" # total attempts to wait for ES
es_wait_interval="${ES_WAIT_INTERVAL:-2}" # seconds between attempts
echo "[i] 查询 ES 端点:$ES"
wait_for_es() {
local attempt=1
while (( attempt <= es_wait_attempts )); do
# 等待集群达到至少 yellow 状态;请求失败则重试
if curl -fsS "$ES/_cluster/health?wait_for_status=yellow&timeout=1s" >/dev/null 2>&1; then
echo "[ok] Elasticsearch 已就绪 (attempt=${attempt}/${es_wait_attempts})"
return 0
fi
echo "[..] 等待 Elasticsearch 可用中 (${attempt}/${es_wait_attempts})"
sleep "${es_wait_interval}"
(( attempt++ ))
done
echo "[err] Elasticsearch 在 ${es_wait_attempts} 次尝试后仍不可用"
return 1
}
safe_count() {
# 对缺失索引返回 0,避免 404 触发失败
local pattern="$1"
local json
json=$(curl -fsS "$ES/${pattern}/_count?ignore_unavailable=true&allow_no_indices=true" 2>/dev/null || echo '{}')
echo "$json" | sed -E 's/.*"count":([0-9]+).*/\1/' | awk 'NF{print $0;exit} END{if(NR==0)print 0}'
}
wait_for_es
# 列出相关索引(可能为空,允许)
curl -fsS "$ES/_cat/indices?v" | egrep 'train-|infer-|logstash' || true
printf "train-* 计数:"; curl -fsS "$ES/train-*/_count" | sed -E 's/.*"count":([0-9]+).*/\1/'; echo
printf "infer-* 计数:"; curl -fsS "$ES/infer-*/_count" | sed -E 's/.*"count":([0-9]+).*/\1/'; echo
# 打印计数,缺失索引按 0 处理
printf "train-* 计数:"; safe_count "train-*"; echo
printf "infer-* 计数:"; safe_count "infer-*"; echo

View File

@@ -19,7 +19,7 @@ get_log_count() {
# 函数:等待服务就绪
wait_for_services() {
echo "[INFO] Waiting for all services to be ready..."
local max_attempts=60
local max_attempts=${SERVICE_WAIT_ATTEMPTS:-120}
local attempt=1
while [ $attempt -le $max_attempts ]; do

View File

@@ -3,23 +3,79 @@ FROM python:3.11-slim
SHELL ["/bin/bash", "-c"]
ARG PIP_INDEX_URL=
ARG USE_OFFLINE=0
ARG USE_INTRANET=false
ARG ARGUS_BUILD_UID=2133
ARG ARGUS_BUILD_GID=2015
ENV ARGUS_BUILD_UID=${ARGUS_BUILD_UID} \
ARGUS_BUILD_GID=${ARGUS_BUILD_GID}
ENV PIP_NO_CACHE_DIR=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app
USER root
WORKDIR /app
COPY requirements.txt ./
COPY ./src/master/requirements.txt ./requirements.txt
COPY ./src/master/offline_wheels/ /opt/offline_wheels/
RUN set -euxo pipefail \
&& python -m pip install --upgrade pip \
&& if [[ -n "$PIP_INDEX_URL" ]]; then \
&& if [[ "$USE_OFFLINE" == "1" ]]; then \
python -m pip install --no-index --find-links /opt/offline_wheels pip && \
python -m pip install --no-index --find-links /opt/offline_wheels -r requirements.txt; \
else \
python -m pip install --upgrade pip && \
if [[ -n "$PIP_INDEX_URL" ]]; then \
PIP_INDEX_URL="$PIP_INDEX_URL" python -m pip install -r requirements.txt; \
else \
python -m pip install -r requirements.txt; \
fi; \
fi
COPY app ./app
# 配置内网 apt 源并安装常用工具
RUN if [[ "$USE_INTRANET" == "true" ]]; then \
echo "Configuring intranet apt sources" && \
if [[ -f /etc/apt/sources.list ]]; then cp /etc/apt/sources.list /etc/apt/sources.list.bak; fi && \
mkdir -p /etc/apt && \
echo "deb [trusted=yes] http://10.68.64.1/ubuntu2204/ jammy main" > /etc/apt/sources.list && \
rm -rf /etc/apt/sources.list.d && \
echo 'Acquire::https::Verify-Peer "false";' > /etc/apt/apt.conf.d/99disable-ssl-check && \
echo 'Acquire::https::Verify-Host "false";' >> /etc/apt/apt.conf.d/99disable-ssl-check; \
fi && \
apt-get update && \
apt-get install -y supervisor net-tools inetutils-ping && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 运行期切换到运行所需的 apt 源
RUN if [[ "$USE_INTRANET" == "true" ]]; then \
echo "deb [trusted=yes] https://10.92.132.52/mirrors/ubuntu2204/ jammy main" > /etc/apt/sources.list; \
fi
RUN mkdir -p /var/log/supervisor
RUN set -eux; \
if getent group argus >/dev/null; then \
groupmod -g "${ARGUS_BUILD_GID}" argus; \
else \
groupadd -g "${ARGUS_BUILD_GID}" argus; \
fi; \
if id argus >/dev/null 2>&1; then \
usermod -u "${ARGUS_BUILD_UID}" -g "${ARGUS_BUILD_GID}" argus; \
else \
useradd -m -u "${ARGUS_BUILD_UID}" -g "${ARGUS_BUILD_GID}" -s /bin/bash argus; \
fi
COPY ./src/master/build/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY ./src/master/build/start-master.sh /usr/local/bin/start-master.sh
COPY ./src/master/build/dns-monitor.sh /usr/local/bin/dns-monitor.sh
RUN chmod +x /usr/local/bin/start-master.sh /usr/local/bin/dns-monitor.sh
COPY ./src/master/app ./app
EXPOSE 3000
CMD ["gunicorn", "--bind", "0.0.0.0:3000", "app:create_app()"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -1,48 +1,186 @@
# Argus Master Module
# Argus Master 模块
A lightweight Flask + SQLite service that manages Argus agent nodes. It exposes node registration, status updates, configuration management, statistics, and generates `nodes.json` for the metric module.
Argus Master 是基于 Flask + SQLite 的节点管理服务,负责:
## Build & Run
- 接收 agent 的注册与重注册请求,分配/校验节点 ID。
- 存储节点元数据、配置、健康状态,并根据上报时间计算在线状态。
- 输出仅包含在线节点的 `nodes.json`,供其他模块(如 metric)消费。
- 提供查询、配置更新、统计等 REST API。
## 构建与运行
```bash
cd src/master
./scripts/build_images.sh # builds argus-master:dev
./scripts/build_images.sh # 生成 argus-master:latest 镜像
```
Local run via Docker compose (test stack):
如需离线构建,先在有网环境运行准备脚本:
```bash
cd src/master
./scripts/prepare_offline_wheels.sh --pip-version 25.2 # 可选 --clean
```
脚本会把 `requirements.txt` 及 pip 指定版本全部下载到 `offline_wheels/`。随后将源码目录(含该子目录)与基础镜像一并拷贝到内网,执行:
```bash
cd src/master
./scripts/build_images.sh --offline --tag argus-master:latest
```
若内网缺少 `python:3.11-slim`,请提前在外网 `docker save` 后通过离线介质 `docker load`。
本仓库提供的端到端测试会使用 `src/master/tests/docker-compose.yml` 启动示例环境:
```bash
cd src/master/tests
./scripts/01_up_master.sh
./scripts/01_up_master.sh # 构建镜像并启动容器,监听 http://localhost:31300
```
Service listens on port `3000` (`31300` when using the provided test compose). Key environment variables:
服务日志与数据默认写入 `tests/private/argus/master/`(或自定义的挂载目录)。
- `DB_PATH` (default `/private/argus/master/db.sqlite3`)
- `METRIC_NODES_JSON_PATH` (default `/private/argus/metric/prometheus/nodes.json`)
- `OFFLINE_THRESHOLD_SECONDS`, `ONLINE_THRESHOLD_SECONDS`, `SCHEDULER_INTERVAL_SECONDS`
## 运行时环境变量
## REST API Summary
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `DB_PATH` | `/private/argus/master/db.sqlite3` | SQLite 数据库存放路径。目录会在启动时自动创建。 |
| `METRIC_NODES_JSON_PATH` | `/private/argus/metric/prometheus/nodes.json` | `nodes.json` 输出位置,仅包含在线节点。采用原子写入避免部分文件。 |
| `OFFLINE_THRESHOLD_SECONDS` | `180` | 若距离最近一次上报时间超过该值,调度器会将节点标记为 `offline`。 |
| `ONLINE_THRESHOLD_SECONDS` | `120` | 若最新上报时间距当前不超过该值,则标记为 `online`。范围处于两个阈值之间时保持原状态。 |
| `SCHEDULER_INTERVAL_SECONDS` | `30` | 调度器检查节点状态与刷新 `nodes.json` 的周期。 |
| `NODE_ID_PREFIX` | `A` | 新节点 ID 的前缀,实际 ID 形如 `A1`、`A2`。 |
| `AUTH_MODE` | `disabled` | 预留的认证开关,当前固定为禁用。 |
Base path: `/api/v1/master`
## 进程与监控
- `GET /nodes` — list node summaries
- `GET /nodes/{id}` — node detail
- `POST /nodes` — register/re-register
- `PUT /nodes/{id}/status` — status report (timestamp + health map)
- `PUT /nodes/{id}/config` — update config/labels (partial)
- `GET /nodes/statistics` — totals + grouped counts
- `GET /healthz`, `GET /readyz` — health checks
镜像内通过 `supervisord` 管理进程:
`nodes.json` only contains nodes whose status is `online`.
- `master`:执行 `/usr/local/bin/start-master.sh`,默认以 4 个 Gunicorn worker 监听 `0.0.0.0:3000`;可通过环境变量 `GUNICORN_WORKERS`、`GUNICORN_BIND`、`GUNICORN_EXTRA_ARGS` 调整。
- `dns-monitor`:轮询 `/private/argus/etc/dns.conf`,若发现变更则调用 `/private/argus/etc/update-dns.sh`,日志输出在 `/var/log/supervisor/dns-monitor.log`。
## Tests
镜像构建阶段会安装 `supervisor`/`net-tools`/`inetutils-ping`/`vim` 等基础工具,并在运行前把 apt 源切换到内网镜像,方便容器内进一步运维。
End-to-end tests are Docker based:
## 域名注册与 DNS 联动
- Master 容器启动时会主动执行 `/private/argus/etc/update-dns.sh`(若存在),把自身 `/etc/resolv.conf` 指向 bind 服务提供的 DNS;随后解析 `eth0` 的 IPv4 地址并写入 `/private/argus/etc/master.argus.com`。该文件会被 bind 模块的 `argus_dns_sync.sh` 监控,用于生成 `master.argus.com` → 当前容器 IP 的 A 记录。
- 测试与生产都需要将 bind 下发的 `update-dns.sh`、`dns.conf` 等文件挂载到 `/private/argus/etc/`。在 E2E 场景中,`tests/private/argus/etc` 会由脚本自动准备。
- 其他模块(如 agent)在启动脚本中只需执行同一份 `update-dns.sh`,即可使用域名访问 master;若域名注册异常,agent 将无法成功上报,可据此快速定位问题。
## REST API 详解
基础路径:`/api/v1/master`,全部返回 JSON。
### 1. `GET /nodes`
- **用途**:获取所有节点的简要信息。
- **响应示例**:
```json
[
{"id": "A1", "name": "dev-user-inst-pod-0", "status": "online", "type": "agent", "version": "1.1.0"}
]
```
### 2. `GET /nodes/{id}`
- **用途**:获取节点详情(包含配置、健康、持久化时间戳等)。
- **错误**:`404` 表示节点不存在。
### 3. `POST /nodes`
- **用途**:注册或重注册节点。
- **请求体**:
```json
{
"id": "A1", // 可选,重注册时携带
"name": "dev-user-inst-pod-0",
"type": "agent",
"version": "1.1.0",
"meta_data": {
"hostname": "dev-user-inst-pod-0",
"ip": "10.0.0.10",
"env": "dev",
"user": "testuser",
"instance": "testinst",
"cpu_number": 4,
"memory_in_bytes": 2147483648,
"gpu_number": 0
}
}
```
- **成功返回**:
- 新节点:`201 Created`,返回完整节点对象。
- 重注册:`200 OK`,返回更新后的节点对象。
- **错误情况**:
- `404 Not Found`:携带的 ID 在 Master 中不存在。
- `500 Internal Server Error`:携带的 ID 与已有名称不匹配。
- `400 Bad Request`:请求体缺字段或类型不正确。
### 4. `PUT /nodes/{id}/status`
- **用途**:Agent 上报状态。Master 记录 `last_report`(服务器时间)与 `agent_last_report`(上报内时间),并更新 `health` 字段。
- **请求体示例**:
```json
{
"timestamp": "2025-09-24T03:24:59Z",
"health": {
"log-fluentbit": {"status": "healthy"},
"metric-node-exporter": {"status": "healthy"}
}
}
```
- **响应**:`200 OK`,返回最新节点对象。`404` 表示节点不存在。
### 5. `PUT /nodes/{id}/config`
- **用途**:局部更新节点配置与标签。
- **请求体示例**:
```json
{
"config": {"log_level": "debug"},
"label": ["gpu", "exp001"]
}
```
- **说明**:字段可任选其一;未提供的配置保持原值。更新标签会触发 `nodes.json` 重新生成。
- **错误**:`404` 表示节点不存在;`400` 表示请求体不合法。
### 6. `GET /nodes/statistics`
- **用途**:统计节点总数及按状态分布。
- **响应示例**:
```json
{
"total": 2,
"status_statistics": [
{"status": "online", "count": 1},
{"status": "offline", "count": 1}
]
}
```
### 7. 健康探针
- `GET /healthz`:进程存活检查。
- `GET /readyz`:数据库可用性检查(会尝试访问 `DB_PATH`)。
如需验证离线镜像,可使用自动化脚本:
```bash
cd src/master/tests
./scripts/00_e2e_test.sh
./scripts/00_e2e_test_offline.sh # 构建离线镜像并执行完整 E2E
```
Scripts create temporary `private/` and `tmp/` directories under `tests/`; they are cleaned automatically by `06_down.sh`.
## 端到端测试场景
执行 `src/master/tests/scripts/00_e2e_test.sh` 会串联以下用例(脚本 01–10):
1. **01_up_master**:构建镜像、启动容器、初始化目录与卷。
2. **02_verify_ready_and_nodes_json**:轮询 `/readyz`,校验初始 `nodes.json` 为 `[]`。
3. **03_register_via_curl**:模拟 agent 注册,保存返回的节点 ID,并确认节点出现在列表接口中。
4. **04_reregister_and_error_cases**:覆盖重注册成功、携带未知 ID 的 `404`、ID/名称不匹配触发 `500` 等场景。
5. **05_status_report_via_curl**:上报健康信息并验证状态自动从 `initialized`→`online`→`offline`→`online` 的转换。
6. **06_config_update_and_nodes_json**:更新配置/标签,检查 `nodes.json` 中的标签同步,并确保离线节点不会出现在文件里。
7. **07_stats_single_node**:等待节点掉线,验证统计接口与 `nodes.json` 为空列表。
8. **08_multi_node_stats**:注册第二节点,使一在线一离线,校验统计聚合和 `nodes.json` 仅包含在线节点。
9. **09_restart_persistence**:重启 master 容器,确认节点数据、统计结果与 `nodes.json` 在持久化目录中保持不变。
10. **10_down**:停止并清理容器、网络与临时目录。
## 相关持久化文件
- SQLite:默认位于 `DB_PATH`,包含 `nodes` 与 `kv` 两张表。
- `nodes.json`:由调度器周期生成,仅保留状态为 `online` 的节点信息。
- 测试用例中的 `tests/private/`、`tests/tmp/` 会随脚本自动清理,避免污染后续运行。
如需在生产环境运行,可将镜像推送到私有仓库,或参考测试 Compose 配置自行部署;只需确保上述环境变量在容器内正确设置即可。

View File

@@ -0,0 +1 @@
../../bind/build/dns-monitor.sh

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -euo pipefail
# 中文提示:确保共享目录与 DNS 相关脚本存在
DNS_DIR="/private/argus/etc"
DNS_SCRIPT="${DNS_DIR}/update-dns.sh"
MASTER_DOMAIN_FILE="${DNS_DIR}/master.argus.com"
RUNTIME_USER="${ARGUS_RUNTIME_USER:-argus}"
RUNTIME_UID="${ARGUS_BUILD_UID:-2133}"
RUNTIME_GID="${ARGUS_BUILD_GID:-2015}"
MASTER_DATA_DIR="/private/argus/master"
METRIC_DIR="/private/argus/metric/prometheus"
mkdir -p "$DNS_DIR"
chown -R "$RUNTIME_UID:$RUNTIME_GID" "$DNS_DIR" 2>/dev/null || true
mkdir -p "$MASTER_DATA_DIR"
mkdir -p "$METRIC_DIR"
chown -R "$RUNTIME_UID:$RUNTIME_GID" "$MASTER_DATA_DIR" "$METRIC_DIR" 2>/dev/null || true
if [[ -x "$DNS_SCRIPT" ]]; then
echo "[INFO] Running update-dns.sh before master starts"
# 中文提示:若脚本存在则执行,保证容器使用 bind 作为 DNS
"$DNS_SCRIPT" || echo "[WARN] update-dns.sh execution failed"
else
echo "[WARN] DNS update script not found or not executable: $DNS_SCRIPT"
fi
# 中文提示:记录 master 当前 IP,供 bind 服务同步
MASTER_IP=$(ifconfig | grep -A 1 eth0 | grep inet | awk '{print $2}' || true)
if [[ -n "${MASTER_IP}" ]]; then
echo "current IP: ${MASTER_IP}"
echo "${MASTER_IP}" > "$MASTER_DOMAIN_FILE"
chown "$RUNTIME_UID:$RUNTIME_GID" "$MASTER_DOMAIN_FILE" 2>/dev/null || true
else
echo "[WARN] Failed to detect master IP via ifconfig"
fi
WORKERS=${GUNICORN_WORKERS:-4}
BIND_ADDR=${GUNICORN_BIND:-0.0.0.0:3000}
EXTRA_OPTS=${GUNICORN_EXTRA_ARGS:-}
if [[ -n "$EXTRA_OPTS" ]]; then
read -r -a EXTRA_ARRAY <<< "$EXTRA_OPTS"
else
EXTRA_ARRAY=()
fi
command=(gunicorn --bind "$BIND_ADDR" --workers "$WORKERS")
if [[ ${#EXTRA_ARRAY[@]} -gt 0 ]]; then
command+=("${EXTRA_ARRAY[@]}")
fi
command+=("app:create_app()")
if command -v runuser >/dev/null 2>&1; then
exec runuser -u "$RUNTIME_USER" -- "${command[@]}"
else
printf -v _cmd_str '%q ' "${command[@]}"
exec su -s /bin/bash -m "$RUNTIME_USER" -c "exec ${_cmd_str}"
fi

View File

@@ -0,0 +1,39 @@
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
user=root
[program:master]
command=/usr/local/bin/start-master.sh
user=root
stdout_logfile=/var/log/supervisor/master.log
stderr_logfile=/var/log/supervisor/master_error.log
autostart=true
autorestart=true
startsecs=5
stopwaitsecs=30
killasgroup=true
stopasgroup=true
[program:dns-monitor]
command=/usr/local/bin/dns-monitor.sh
user=root
stdout_logfile=/var/log/supervisor/dns-monitor.log
stderr_logfile=/var/log/supervisor/dns-monitor_error.log
autostart=true
autorestart=true
startsecs=5
stopwaitsecs=10
killasgroup=true
stopasgroup=true
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

Binary file not shown.

View File

@@ -2,19 +2,42 @@
set -euo pipefail
usage() {
echo "Usage: $0 [--intranet] [--tag <image_tag>]" >&2
cat >&2 <<'USAGE'
Usage: $0 [--intranet] [--offline] [--tag <image_tag>]
Options:
--intranet 使用指定的 PyPI 镜像源(默认清华镜像)。
--offline 完全离线构建,依赖 offline_wheels/ 目录中的离线依赖包。
--tag <image_tag> 自定义镜像标签,默认 argus-master:latest。
USAGE
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
IMAGE_TAG="${IMAGE_TAG:-argus-master:dev}"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
MODULE_ROOT="$PROJECT_ROOT/src/master"
IMAGE_TAG="${IMAGE_TAG:-argus-master:latest}"
DOCKERFILE="src/master/Dockerfile"
BUILD_ARGS=()
OFFLINE_MODE=0
source "$PROJECT_ROOT/scripts/common/build_user.sh"
load_build_user
BUILD_ARGS+=("--build-arg" "ARGUS_BUILD_UID=${ARGUS_BUILD_UID}" "--build-arg" "ARGUS_BUILD_GID=${ARGUS_BUILD_GID}")
cd "$PROJECT_ROOT"
while [[ "$#" -gt 0 ]]; do
case "$1" in
--intranet)
INTRANET_INDEX="${INTRANET_INDEX:-https://pypi.tuna.tsinghua.edu.cn/simple}"
BUILD_ARGS+=("--build-arg" "PIP_INDEX_URL=${INTRANET_INDEX}")
BUILD_ARGS+=("--build-arg" "USE_INTRANET=true")
shift
;;
--offline)
OFFLINE_MODE=1
BUILD_ARGS+=("--build-arg" "USE_OFFLINE=1")
BUILD_ARGS+=("--build-arg" "USE_INTRANET=true")
shift
;;
--tag)
@@ -34,6 +57,20 @@ while [[ "$#" -gt 0 ]]; do
esac
done
if [[ "$OFFLINE_MODE" -eq 1 ]]; then
WHEELS_DIR="$MODULE_ROOT/offline_wheels"
if [[ ! -d "$WHEELS_DIR" ]]; then
echo "[ERROR] offline_wheels 目录不存在: $WHEELS_DIR" >&2
exit 1
fi
if ! find "$WHEELS_DIR" -maxdepth 1 -type f -name '*.whl' -print -quit >/dev/null; then
echo "[ERROR] offline_wheels 目录为空,请先在有网环境执行 scripts/prepare_offline_wheels.sh" >&2
exit 1
fi
fi
echo "[INFO] Building image $IMAGE_TAG"
docker build "${BUILD_ARGS[@]}" -t "$IMAGE_TAG" "$PROJECT_ROOT"
docker build -f "$DOCKERFILE" "${BUILD_ARGS[@]}" -t "$IMAGE_TAG" "$PROJECT_ROOT"
echo "[OK] Image $IMAGE_TAG built"

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'USAGE'
Usage: $0 [--pip-version <version>] [--clean] [--local]
Options:
--pip-version <version> 额外下载指定版本的 pip wheel(例如 25.2)。
--clean 清理 offline_wheels/*.whl 后重新下载。
--local 使用本地 python 执行下载(默认通过 docker python:3.11-slim)。
USAGE
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REQUIREMENTS_FILE="$PROJECT_ROOT/requirements.txt"
WHEEL_DIR="$PROJECT_ROOT/offline_wheels"
PIP_VERSION=""
CLEAN=0
USE_LOCAL=0
while [[ $# -gt 0 ]]; do
case "$1" in
--pip-version)
[[ $# -ge 2 ]] || { usage; exit 1; }
PIP_VERSION="$2"
shift 2
;;
--clean)
CLEAN=1
shift
;;
--local)
USE_LOCAL=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
done
if [[ ! -f "$REQUIREMENTS_FILE" ]]; then
echo "[ERROR] requirements.txt not found at $REQUIREMENTS_FILE" >&2
exit 1
fi
mkdir -p "$WHEEL_DIR"
if [[ "$CLEAN" -eq 1 ]]; then
echo "[INFO] Cleaning existing wheels in $WHEEL_DIR"
find "$WHEEL_DIR" -maxdepth 1 -type f -name '*.whl' -delete
fi
run_with_python() {
local cmd=("python" "-m" "pip" "$@")
eval "${cmd[@]}"
}
if [[ "$USE_LOCAL" -eq 1 ]]; then
PYTHON_BIN=${PYTHON_BIN:-python3}
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
echo "[ERROR] $PYTHON_BIN not found" >&2
exit 1
fi
echo "[INFO] Using local python ($PYTHON_BIN) to download wheels"
"$PYTHON_BIN" -m pip download -r "$REQUIREMENTS_FILE" -d "$WHEEL_DIR"
if [[ -n "$PIP_VERSION" ]]; then
"$PYTHON_BIN" -m pip download "pip==${PIP_VERSION}" -d "$WHEEL_DIR"
fi
else
if ! command -v docker >/dev/null 2>&1; then
echo "[ERROR] docker not found; rerun with --local or安装 docker" >&2
exit 1
fi
echo "[INFO] Using docker image python:3.11-slim 下载 wheel"
docker run --rm \
-v "$WHEEL_DIR":/wheels \
-v "$REQUIREMENTS_FILE":/tmp/requirements.txt \
python:3.11-slim \
bash -c "set -euo pipefail && python -m pip install --upgrade pip && python -m pip download -r /tmp/requirements.txt -d /wheels"
if [[ -n "$PIP_VERSION" ]]; then
docker run --rm \
-v "$WHEEL_DIR":/wheels \
python:3.11-slim \
bash -c "set -euo pipefail && python -m pip download pip==${PIP_VERSION} -d /wheels"
fi
fi
echo "[INFO] Offline wheels prepared at $WHEEL_DIR"

View File

@@ -8,7 +8,7 @@ usage() {
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEFAULT_OUTPUT="$PROJECT_ROOT/images/argus-master-dev.tar"
IMAGE_TAG="${IMAGE_TAG:-argus-master:dev}"
IMAGE_TAG="${IMAGE_TAG:-argus-master:latest}"
OUTPUT_PATH="$DEFAULT_OUTPUT"
while [[ "$#" -gt 0 ]]; do

View File

@@ -1,6 +1,6 @@
services:
master:
image: argus-master:dev
image: ${MASTER_IMAGE_TAG:-argus-master:latest}
container_name: argus-master-e2e
environment:
- OFFLINE_THRESHOLD_SECONDS=6
@@ -11,6 +11,7 @@ services:
volumes:
- ./private/argus/master:/private/argus/master
- ./private/argus/metric/prometheus:/private/argus/metric/prometheus
- ./private/argus/etc:/private/argus/etc
restart: unless-stopped
networks:

View File

@@ -17,7 +17,7 @@ SCRIPTS=(
for script in "${SCRIPTS[@]}"; do
echo "[TEST] Running $script"
"$SCRIPT_DIR/$script"
MASTER_IMAGE_TAG="${MASTER_IMAGE_TAG:-argus-master:latest}" "$SCRIPT_DIR/$script"
echo "[TEST] $script completed"
echo
done

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODULE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MASTER_ROOT="$(cd "$MODULE_ROOT/.." && pwd)"
# 准备离线依赖并构建镜像
pushd "$MASTER_ROOT" >/dev/null
./scripts/prepare_offline_wheels.sh --clean --pip-version 25.2
./scripts/build_images.sh --offline --tag argus-master:offline
popd >/dev/null
# 使用离线镜像执行既有端到端用例
MASTER_IMAGE_TAG="argus-master:offline" ./scripts/00_e2e_test.sh

View File

@@ -6,10 +6,14 @@ TEST_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
MODULE_ROOT="$(cd "$TEST_ROOT/.." && pwd)"
PRIVATE_ROOT="$TEST_ROOT/private"
TMP_ROOT="$TEST_ROOT/tmp"
DNS_ROOT="$PRIVATE_ROOT/argus/etc"
BIND_UPDATE_SCRIPT_SRC="$(cd "$MODULE_ROOT/../bind" && pwd)/build/update-dns.sh"
BIND_UPDATE_SCRIPT_DEST="$DNS_ROOT/update-dns.sh"
mkdir -p "$PRIVATE_ROOT/argus/master"
mkdir -p "$PRIVATE_ROOT/argus/metric/prometheus"
mkdir -p "$TMP_ROOT"
mkdir -p "$DNS_ROOT"
# 确保上一次运行留下的容器/数据被清理
compose() {
@@ -28,14 +32,19 @@ rm -rf "$TMP_ROOT" "$PRIVATE_ROOT"
mkdir -p "$PRIVATE_ROOT/argus/master"
mkdir -p "$PRIVATE_ROOT/argus/metric/prometheus"
mkdir -p "$TMP_ROOT"
mkdir -p "$DNS_ROOT"
pushd "$MODULE_ROOT" >/dev/null
./scripts/build_images.sh --tag argus-master:dev
popd >/dev/null
# 中文提示:将 bind 模块自带的 update-dns.sh 下发到共享目录,模拟实际环境
if [[ -f "$BIND_UPDATE_SCRIPT_SRC" ]]; then
cp "$BIND_UPDATE_SCRIPT_SRC" "$BIND_UPDATE_SCRIPT_DEST"
chmod +x "$BIND_UPDATE_SCRIPT_DEST"
else
echo "[WARN] bind update script missing at $BIND_UPDATE_SCRIPT_SRC"
fi
pushd "$TEST_ROOT" >/dev/null
compose down --remove-orphans || true
compose up -d
MASTER_IMAGE_TAG="${MASTER_IMAGE_TAG:-argus-master:latest}" compose up -d
popd >/dev/null
echo "[INFO] Master container is up on http://localhost:31300"

View File

@@ -6,6 +6,7 @@ TEST_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PRIVATE_ROOT="$TEST_ROOT/private"
API_BASE="http://localhost:31300"
NODES_JSON_PATH="$PRIVATE_ROOT/argus/metric/prometheus/nodes.json"
MASTER_DOMAIN_FILE="$PRIVATE_ROOT/argus/etc/master.argus.com"
# 等待 readyz 返回 200,确保数据库初始化完成
for _ in {1..30}; do
@@ -49,3 +50,11 @@ then
fi
echo "[INFO] nodes.json 初始状态校验通过"
# 中文提示:输出 master 写入的域名文件,失败不影响测试
if [[ -f "$MASTER_DOMAIN_FILE" ]]; then
MASTER_IP=$(<"$MASTER_DOMAIN_FILE")
echo "[INFO] master.argus.com 记录: $MASTER_IP"
else
echo "[WARN] 未找到 master.argus.com 记录文件,目录=$MASTER_DOMAIN_FILE"
fi

View File

@@ -93,7 +93,6 @@ keys = [
"health",
"last_report",
"agent_last_report",
"status",
]
for key in keys:
if before.get(key) != after.get(key):
@@ -119,13 +118,37 @@ keys = [
"health",
"last_report",
"agent_last_report",
"status",
]
for key in keys:
if before.get(key) != after.get(key):
raise AssertionError(f"Key {key} changed after restart: {before.get(key)} -> {after.get(key)}")
PY
payload=$(python3 - <<'PY'
import json
from datetime import datetime, timezone
body = {
"timestamp": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
"health": {
"log-fluentbit": {"status": "healthy"}
}
}
print(json.dumps(body))
PY
)
curl -sS -o "$TMP_ROOT/restart_second_status.json" -w '%{http_code}' \
-H 'Content-Type: application/json' -X PUT \
"$API_BASE/nodes/$SECOND_NODE_ID/status" -d "$payload" > "$TMP_ROOT/restart_second_status_code"
if [[ $(cat "$TMP_ROOT/restart_second_status_code") != "200" ]]; then
echo "[ERROR] Failed to restore second node status post-restart" >&2
cat "$TMP_ROOT/restart_second_status.json" >&2
exit 1
fi
sleep 3
# 对比重启前后的 nodes.json 与统计信息,验证持久化一致性
nodes_json_after="$TMP_ROOT/nodes_json_post_restart.json"
cp "$PRIVATE_ROOT/argus/metric/prometheus/nodes.json" "$nodes_json_after"

7
src/metric/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/prometheus/data/
/client-plugins/dcgm-exporter-installer/
/client-plugins/demo-all-in-one/artifact/
/client-plugins/demo-all-in-one/publish/
/client-plugins/demo-all-in-one/checklist
/client-plugins/demo-all-in-one/VERSION
/client-plugins/all-in-one-full/

View File

@@ -0,0 +1,65 @@
# 客户侧组件安装包构建、发布流程
## 第一步:配置版本和组件
首先搞定配置文件:
1. 把 `.checklist.example` 重命名成 `checklist`
2. 把 `.VERSION.example` 重命名成 `VERSION`
### checklist 文件格式
```
# 组件名称 目录路径 版本号 [依赖组件] [安装顺序]
dcgm-exporter-installer /path/to/dcgm-exporter-installer 1.1.0
node-exporter-installer /path/to/node-exporter-installer 1.1.0
```
### VERSION 文件
设置需要发布的版本号,比如 `1.29.0`
> 建议用 `version-manager.sh` 来管理版本
## 第二步:构建安装包
直接跑脚本:
```bash
./package_artifact.sh
```
构建完的东西会放在 `artifact/` 目录下,按版本分文件夹。
如果版本已经存在了,想要覆盖重新构建:
```bash
./package_artifact.sh --force
```
构建完可以手工测试安装包。
## 第三步:发布安装包
用这个脚本发布:
```bash
./publish_artifact.sh
```
发布后的内容在 `publish/` 目录里,包含:
- 压缩版本的安装包
- 一键安装的bash脚本
## 第四步:部署到FTP服务器
把发布的内容上传到FTP服务器,客户端就可以通过一键命令安装:
```bash
curl -fsSL 'ftp://{$USER}:{$PASSWD}@{$your-ftp-server}/setup.sh' -o setup.sh
# root用户直接执行,非root用户需要使用sudo
chmod +x setup.sh
bash setup.sh --server {$your-ftp-server} --user {$USER} --password {$PASSWD}
示例:
curl -fsS 'ftp://ftpuser:ZGClab1234!@177.177.70.200/setup.sh' -o setup.sh
chmod +x setup.sh
bash setup.sh --server {$域名} --user ftpuser --password 'ZGClab1234!'
```

View File

@@ -0,0 +1 @@
1.29.0

View File

@@ -0,0 +1,3 @@
# 组件名称 目录路径 版本号 [依赖组件] [安装顺序]
dcgm-exporter-installer /Users/sundapeng/Project/nlp/aiops/client-plugins/dcgm-exporter-installer 1.1.0
node-exporter-installer /Users/sundapeng/Project/nlp/aiops/client-plugins/node-exporter-installer 1.1.0

View File

@@ -0,0 +1,8 @@
# Argus Metric 配置文件示例
# 复制此文件为 config.env 并根据需要修改配置
# 连接master服务
MASTER_ENDPOINT=master.argus.com:3000
# 上报状态间隔描述(秒)
REPORT_INTERVAL_SECONDS=60

View File

@@ -0,0 +1,3 @@
# Elasticsearch
ES_HOST=es.log.argus.com
ES_PORT=9200

View File

@@ -0,0 +1 @@
177.177.17.106

View File

@@ -0,0 +1,55 @@
#!/bin/bash
# Node Exporter 健康检查脚本
# 输出 JSON 格式结果
set -e
# 检查 Node Exporter 健康状态
check_health() {
local url="http://localhost:9100"
local metrics_url="$url/metrics"
local name="node-exporter"
local status="unhealth"
local reason=""
# 检查 curl 是否可用
if ! command -v curl &> /dev/null; then
reason="curl 命令不可用,无法进行健康检查"
echo "{\"name\": \"$name\", \"status\": \"$status\", \"reason\": \"$reason\"}"
exit 1
fi
# 测试根路径连接
local http_code=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
if [[ "$http_code" == "200" ]]; then
# 测试 metrics 端点
local metrics_code=$(curl -s -o /dev/null -w "%{http_code}" "$metrics_url" 2>/dev/null || echo "000")
if [[ "$metrics_code" == "200" ]]; then
status="health"
reason="success"
echo "{\"name\": \"$name\", \"status\": \"$status\", \"reason\": \"$reason\"}"
exit 0
else
reason="Metrics 端点异常 (HTTP $metrics_code)"
echo "{\"name\": \"$name\", \"status\": \"$status\", \"reason\": \"$reason\"}"
exit 1
fi
else
reason="HTTP 服务异常 (HTTP $http_code),请检查 Node Exporter 是否正在运行在端口 9100"
echo "{\"name\": \"$name\", \"status\": \"$status\", \"reason\": \"$reason\"}"
exit 1
fi
}
# 主函数
main() {
check_health
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,343 @@
#!/bin/bash
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 更新安装记录
update_install_record() {
local pid="$1"
# 使用传入的安装目录参数,如果没有则使用默认值
local install_base_dir="${2:-/opt/argus-metric/current}"
local install_record="$install_base_dir/.install_record"
# 如果安装记录文件不存在,说明是首次安装,由主安装脚本统一创建
if [[ ! -f "$install_record" ]]; then
log_info "安装记录文件不存在,将由主安装脚本创建"
return 0
fi
# 如果文件存在,说明是重启场景,只更新 PID 字段
if command -v jq &> /dev/null; then
# 读取当前 PID
local current_pid=$(jq -r '.components."node-exporter".pid // ""' "$install_record" 2>/dev/null)
if [[ -z "$current_pid" ]]; then
log_warning "无法读取当前 PID,跳过更新"
return 1
fi
# 使用 jq 只更新 pid 字段,保持字符串类型,保留其他字段
jq --arg new_pid "$pid" '.components."node-exporter".pid = $new_pid' "$install_record" > "$install_record.tmp" && mv "$install_record.tmp" "$install_record"
log_info "PID 已更新: $current_pid -> $pid"
else
log_warning "jq 命令不可用,无法更新安装记录文件"
fi
}
# 显示帮助信息
show_help() {
echo "Node Exporter 安装脚本"
echo
echo "用法: $0 [选项]"
echo
echo "选项:"
echo " --help 显示此帮助信息"
echo
echo "示例:"
echo " $0 # 安装 Node Exporter"
echo
}
# 解析命令行参数
INSTALL_DIR=""
for arg in "$@"; do
case $arg in
--help|-h)
show_help
exit 0
;;
*)
# 如果参数不是以--开头,则认为是安装目录
if [[ ! "$arg" =~ ^-- ]]; then
INSTALL_DIR="$arg"
else
log_error "未知参数: $arg"
show_help
exit 1
fi
;;
esac
done
# 检查是否为 root 用户
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "此脚本需要 root 权限运行"
log_info "请使用: sudo $0"
exit 1
fi
}
# 检查系统要求
check_system() {
log_info "检查系统要求..."
# 检查操作系统
if [[ ! -f /etc/os-release ]]; then
log_error "无法检测操作系统版本"
exit 1
fi
source /etc/os-release
log_info "检测到操作系统: $NAME $VERSION"
# 检查是否为 Linux 系统
if [[ "$ID" != "ubuntu" && "$ID" != "debian" && "$ID" != "centos" && "$ID" != "rhel" && "$ID" != "fedora" ]]; then
log_warning "此脚本主要针对常见 Linux 发行版,其他系统可能需要调整"
fi
# 检查系统架构
local arch=$(uname -m)
log_info "系统架构: $arch"
if [[ "$arch" != "x86_64" && "$arch" != "amd64" ]]; then
log_warning "当前架构为 $arch,node_exporter 主要支持 x86_64/amd64"
fi
}
stop_existing_service() {
log_info "检查并停止可能运行的 Node Exporter 服务..."
# 当前脚本 PID,防止误杀
SELF_PID=$$
# 1. 停止 systemd 服务(如果存在)
if systemctl list-units --full -all | grep -q "node_exporter.service"; then
log_info "检测到 systemd 服务 node_exporter,正在停止..."
systemctl stop node_exporter || true
systemctl disable node_exporter || true
fi
# 2. 清理可能存在的 PID 文件
for pid_file in /var/run/node-exporter.pid /var/run/node_exporter.pid /tmp/node_exporter.pid; do
if [[ -f "$pid_file" ]]; then
local pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
log_info "发现 Node Exporter (PID: $pid),正在停止..."
kill "$pid"
sleep 2
kill -0 "$pid" 2>/dev/null && kill -9 "$pid"
fi
rm -f "$pid_file"
fi
done
# 3. 用 pgrep 查找进程,排除当前脚本
local pids=$(pgrep -f "node_exporter|node-exporter|/usr/local/bin/node-exporter" | grep -vw "$SELF_PID" || true)
if [[ -n "$pids" ]]; then
log_info "发现 Node Exporter 进程 (PID: $pids),正在停止..."
for pid in $pids; do
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
sleep 1
kill -0 "$pid" 2>/dev/null && kill -9 "$pid" 2>/dev/null || true
fi
done
fi
# 4. 兜底:检查是否有进程占用 9100 端口
local listen_pids=$(lsof -ti:9100 2>/dev/null || true)
if [[ -n "$listen_pids" ]]; then
log_warning "发现占用 9100 端口的进程 (PID: $listen_pids),强制终止..."
for pid in $listen_pids; do
kill -9 "$pid" 2>/dev/null || true
done
sleep 1
fi
# 5. 最终验证
if netstat -tuln 2>/dev/null | grep -q ":9100 "; then
log_error "端口 9100 仍被占用,请手动检查"
return 1
else
log_success "旧的 Node Exporter 已完全停止"
fi
}
# 安装 Node Exporter 二进制文件
install_node_exporter() {
log_info "安装 Node Exporter..."
local binary_file="bin/node_exporter"
local install_dir="/usr/local/bin"
if [[ ! -f "$binary_file" ]]; then
log_error "找不到 Node Exporter 二进制文件: $binary_file"
exit 1
fi
# 停止可能运行的服务
stop_existing_service
# 复制二进制文件并重命名为统一格式
cp "$binary_file" "$install_dir/node-exporter"
chmod +x "$install_dir/node-exporter"
log_success "Node Exporter 二进制文件安装完成"
}
# 创建用户和组
create_user() {
log_info "创建 node_exporter 用户..."
# 检查用户是否已存在
if id "node_exporter" &>/dev/null; then
log_info "用户 node_exporter 已存在"
else
useradd --no-create-home --shell /bin/false node_exporter
log_success "用户 node_exporter 创建完成"
fi
}
# 安装配置文件
install_config() {
log_info "安装配置文件..."
local config_dir="/etc/node_exporter"
# 创建配置目录
mkdir -p "$config_dir"
# 创建文本文件收集器目录
mkdir -p "/var/lib/node_exporter/textfile_collector"
chown node_exporter:node_exporter "/var/lib/node_exporter/textfile_collector"
}
# 启动 Node Exporter 服务
start_node_exporter() {
log_info "启动 Node Exporter 服务..."
local binary_path="/usr/local/bin/node-exporter"
local log_file="/var/log/node-exporter.log"
local pid_file="/var/run/node-exporter.pid"
# 检查服务是否已经在运行
if [[ -f "$pid_file" ]]; then
local pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
log_info "Node Exporter 服务已在运行 (PID: $pid)"
return 0
else
log_warning "发现过期的 PID 文件,正在清理..."
rm -f "$pid_file"
fi
fi
# 检查端口是否被占用
if netstat -tuln 2>/dev/null | grep -q ":9100 "; then
log_warning "端口 9100 已被占用,请检查是否有其他服务在运行"
return 1
fi
# 启动服务
log_info "正在启动 Node Exporter..."
nohup "$binary_path" --web.listen-address=:9100 > "$log_file" 2>&1 &
local pid=$!
# 保存 PID
echo "$pid" > "$pid_file"
# 等待服务启动
sleep 2
# 检查服务是否成功启动
if kill -0 "$pid" 2>/dev/null; then
log_success "Node Exporter 服务启动成功 (PID: $pid)"
log_info "日志文件: $log_file"
log_info "PID 文件: $pid_file"
# 更新安装记录
update_install_record "$pid" "$INSTALL_DIR"
else
log_error "Node Exporter 服务启动失败"
rm -f "$pid_file"
return 1
fi
}
# 显示安装信息
show_install_info() {
log_success "Node Exporter 安装完成!"
echo
echo "安装信息:"
echo " 二进制文件: /usr/local/bin/node-exporter"
echo " 运行用户: node_exporter"
echo " 配置目录: /etc/node_exporter/"
echo " 默认端口: 9100"
echo
echo "使用方法:"
echo " 手动启动: /usr/local/bin/node-exporter --web.listen-address=:9100"
echo " 后台启动: nohup /usr/local/bin/node-exporter --web.listen-address=:9100 &"
echo
echo "测试连接:"
echo " curl http://localhost:9100/metrics"
echo " curl http://localhost:9100"
echo
echo "Prometheus 配置示例:"
echo " - job_name: 'node_exporter'"
echo " static_configs:"
echo " - targets: ['localhost:9100']"
echo
}
# 主函数
main() {
echo "=========================================="
echo " Node Exporter 安装脚本 v1.0"
echo "=========================================="
echo
check_root
check_system
log_info "开始安装 Node Exporter..."
install_node_exporter
create_user
install_config
start_node_exporter
show_install_info
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,87 @@
#!/bin/bash
set -e
# 颜色定义
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
# 获取当前目录
CURRENT_DIR=$(pwd)
PACKAGE_NAME="node-exporter-$(date +%Y%m%d-%H%M%S)"
PACKAGE_FILE="${PACKAGE_NAME}.tar.gz"
log_info "开始打包 Node Exporter 安装包..."
# 检查必要文件
log_info "检查必要文件..."
required_files=(
"install.sh"
"uninstall.sh"
"bin/node_exporter"
"check_health.sh"
)
missing_files=()
for file in "${required_files[@]}"; do
if [[ ! -f "$file" ]]; then
missing_files+=("$file")
fi
done
if [[ ${#missing_files[@]} -gt 0 ]]; then
echo "缺少以下文件:"
for file in "${missing_files[@]}"; do
echo " - $file"
done
exit 1
fi
log_success "所有必要文件检查完成"
# 创建临时目录
TEMP_DIR=$(mktemp -d)
log_info "创建临时目录: $TEMP_DIR"
# 复制文件到临时目录
cp -r . "$TEMP_DIR/$PACKAGE_NAME"
# 进入临时目录
cd "$TEMP_DIR"
# 创建压缩包
log_info "创建压缩包: $PACKAGE_FILE"
tar -czf "$PACKAGE_FILE" "$PACKAGE_NAME"
# 移动压缩包到原目录
mv "$PACKAGE_FILE" "$CURRENT_DIR/"
# 清理临时目录
rm -rf "$TEMP_DIR"
# 返回原目录
cd "$CURRENT_DIR"
# 显示结果
log_success "打包完成!"
echo
echo "安装包文件: $PACKAGE_FILE"
echo "文件大小: $(du -h "$PACKAGE_FILE" | cut -f1)"
echo
echo "使用方法:"
echo "1. 将 $PACKAGE_FILE 传输到目标服务器"
echo "2. 解压: tar -xzf $PACKAGE_FILE"
echo "3. 进入目录: cd $PACKAGE_NAME"
echo "4. 运行安装: sudo ./install.sh"
echo
echo "注意: 请确保所有必要文件都存在"

View File

@@ -0,0 +1,239 @@
#!/bin/bash
# Node Exporter 卸载脚本
# 版本: 1.0
# 作者: AIOps Team
# 日期: $(date +%Y-%m-%d)
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查是否为 root 用户
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "此脚本需要 root 权限运行"
log_info "请使用: sudo $0"
exit 1
fi
}
# 停止运行中的进程
stop_processes() {
log_info "停止 Node Exporter 进程..."
local pid_file="/var/run/node-exporter.pid"
local stopped=false
# 首先尝试通过 PID 文件停止服务
if [[ -f "$pid_file" ]]; then
local pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
log_info "通过 PID 文件停止服务 (PID: $pid)..."
kill "$pid"
sleep 3
# 检查进程是否已停止
if kill -0 "$pid" 2>/dev/null; then
log_warning "进程未响应,强制终止..."
kill -9 "$pid" 2>/dev/null || true
fi
log_success "Node Exporter 进程已停止"
stopped=true
else
log_warning "PID 文件存在但进程已不存在,清理 PID 文件"
rm -f "$pid_file"
fi
fi
# 查找并杀死所有 node_exporter 和 node-exporter 进程
local pids=$(pgrep -f "node_exporter\|node-exporter" 2>/dev/null || true)
if [[ -n "$pids" ]]; then
log_info "发现 node_exporter 或 node-exporter 进程,正在停止..."
for pid in $pids; do
log_info "停止进程 PID: $pid"
kill "$pid" 2>/dev/null || true
done
sleep 2
# 检查是否还有进程在运行,如果有则强制终止
local remaining_pids=$(pgrep -f "node_exporter\|node-exporter" 2>/dev/null || true)
if [[ -n "$remaining_pids" ]]; then
log_warning "进程未响应,强制终止..."
for pid in $remaining_pids; do
log_info "强制终止进程 PID: $pid"
kill -9 "$pid" 2>/dev/null || true
done
sleep 1
fi
# 最终检查
if pgrep -f "node_exporter\|node-exporter" > /dev/null; then
log_error "无法停止所有 node_exporter 进程"
else
log_success "所有 Node Exporter 进程已停止"
stopped=true
fi
else
log_info "Node Exporter 进程未运行"
fi
# 清理 PID 文件
rm -f "$pid_file"
if [[ "$stopped" == "false" ]]; then
log_warning "未发现需要停止的 Node Exporter 进程"
fi
}
# 删除二进制文件
remove_binary() {
log_info "删除 Node Exporter 二进制文件..."
local binary_files=(
"/usr/local/bin/node-exporter"
"/usr/local/bin/node_exporter"
)
local deleted=false
for binary_file in "${binary_files[@]}"; do
if [[ -f "$binary_file" ]]; then
rm -f "$binary_file"
log_success "二进制文件已删除: $binary_file"
deleted=true
fi
done
if [[ "$deleted" == "false" ]]; then
log_info "二进制文件不存在"
fi
}
# 删除配置文件
remove_config() {
log_info "删除配置文件..."
local config_dir="/etc/node_exporter"
if [[ -d "$config_dir" ]]; then
rm -rf "$config_dir"
log_success "配置目录已删除"
else
log_info "配置目录不存在"
fi
}
# 删除数据目录
remove_data_dir() {
log_info "删除数据目录..."
local data_dir="/var/lib/node_exporter"
if [[ -d "$data_dir" ]]; then
rm -rf "$data_dir"
log_success "数据目录已删除"
else
log_info "数据目录不存在"
fi
}
# 检查用户状态(可选)
check_user_status() {
log_info "检查 node_exporter 用户状态..."
if id "node_exporter" &>/dev/null; then
log_info "检测到 node_exporter 用户存在"
log_warning "node_exporter 是系统用户,可能被其他服务使用"
log_info "为了系统稳定性,将保留 node_exporter 用户"
log_info "如需手动删除,请运行: sudo userdel node_exporter"
else
log_info "node_exporter 用户不存在"
fi
}
# 清理日志文件
cleanup_logs() {
log_info "清理日志文件..."
# 清理 journal 日志
journalctl --vacuum-time=1s --quiet || true
# 删除安装脚本创建的日志文件
rm -f /var/log/node-exporter.log
log_success "日志文件已清理"
}
# 显示卸载信息
show_uninstall_info() {
log_success "Node Exporter 卸载完成!"
echo
echo "已删除的内容:"
echo " - 二进制文件: /usr/local/bin/node-exporter"
echo " - 配置目录: /etc/node_exporter"
echo " - 数据目录: /var/lib/node_exporter"
echo " - 相关日志文件"
echo
echo "注意:"
echo " - node_exporter 用户已保留(系统用户,可能被其他服务使用)"
echo " - 如需完全清理,请手动检查并删除相关文件"
echo
}
# 主函数
main() {
echo "=========================================="
echo " Node Exporter 卸载脚本 v1.0"
echo "=========================================="
echo
check_root
log_warning "此操作将完全卸载 Node Exporter"
read -p "确认继续?(y/N): " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
log_info "取消卸载操作"
exit 0
fi
log_info "开始卸载 Node Exporter..."
stop_processes
remove_binary
remove_config
remove_data_dir
cleanup_logs
# 检查用户状态
check_user_status
show_uninstall_info
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,277 @@
#!/bin/bash
# 整体健康检查脚本,调用各个组件的健康检查并将结果写入 .health_log 文件
set -e
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HEALTH_LOG_FILE="$SCRIPT_DIR/.health_log"
INSTALL_RECORD_FILE="$SCRIPT_DIR/.install_record"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数 - 输出到 stderr 避免影响 JSON 结果
log_info() {
echo -e "${BLUE}[INFO]${NC} $1" >&2
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1" >&2
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# 检查单个组件健康状态
check_component() {
local component_name="$1"
local check_script_path="$2"
log_info "检查 $component_name 健康状态..."
if [[ ! -f "$check_script_path" ]]; then
log_error "健康检查脚本不存在: $check_script_path"
echo "{\"name\": \"$component_name\", \"status\": \"unhealth\", \"reason\": \"健康检查脚本不存在: $check_script_path\"}"
return 1
fi
if [[ ! -x "$check_script_path" ]]; then
log_error "健康检查脚本无执行权限: $check_script_path"
echo "{\"name\": \"$component_name\", \"status\": \"unhealth\", \"reason\": \"健康检查脚本无执行权限: $check_script_path\"}"
return 1
fi
# 执行健康检查脚本,只捕获 stdout,stderr 输出到终端
local result
if result=$("$check_script_path" 2>/dev/null); then
log_success "$component_name 健康检查通过"
echo "$result"
return 0
else
log_warning "$component_name 健康检查失败"
echo "$result"
return 1
fi
}
# 生成时间戳
get_timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
# 生成UTC时间戳
get_utc_timestamp() {
date -u '+%Y-%m-%dT%H:%M:%SZ'
}
# 获取主机名
get_hostname() {
echo "${HOSTNAME:-$(hostname)}"
}
# 创建健康状态目录
create_health_dir() {
local hostname=$(get_hostname)
local health_dir="/private/argus/agent/$hostname/health"
if [[ ! -d "$health_dir" ]]; then
log_info "创建健康状态目录: $health_dir"
mkdir -p "$health_dir"
fi
echo "$health_dir"
}
# 写入单个模块的健康状态JSON文件
write_component_health_json() {
local component_name="$1"
local status="$2"
local error_msg="$3"
local health_dir="$4"
# 生成模块名前缀-xxx.json格式的文件名
local module_prefix="metric"
local filename="${module_prefix}-${component_name}.json"
local filepath="$health_dir/$filename"
# 生成UTC时间戳
local timestamp=$(get_utc_timestamp)
# 构建JSON内容
local json_content=$(cat << EOF
{
"status": "$status",
"error": "$error_msg",
"timestamp": "$timestamp"
}
EOF
)
# 写入文件
echo "$json_content" > "$filepath"
log_info "已写入模块健康状态文件: $filepath"
}
# 从安装记录文件中读取组件安装目录
read_install_record() {
local install_record_file="$1"
if [[ ! -f "$install_record_file" ]]; then
log_error "安装记录文件不存在: $install_record_file"
return 1
fi
# 检查是否有 jq 命令来解析 JSON
if command -v jq &> /dev/null; then
# 使用 jq 解析 JSON
local components_json
if components_json=$(jq -r '.components | to_entries[] | "\(.key):\(.value.install_dir)"' "$install_record_file" 2>/dev/null); then
echo "$components_json"
return 0
else
log_error "无法解析安装记录文件 JSON 格式: $install_record_file"
return 1
fi
else
# 如果没有 jq,尝试简单的文本解析
log_warning "jq 命令不可用,尝试简单文本解析"
# 查找所有 install_dir 行
local components=()
while IFS= read -r line; do
if [[ "$line" =~ \"install_dir\":[[:space:]]*\"([^\"]+)\" ]]; then
local install_dir="${BASH_REMATCH[1]}"
# 从路径中提取组件名称
local component_name=$(basename "$install_dir")
components+=("$component_name:$install_dir")
fi
done < "$install_record_file"
if [[ ${#components[@]} -gt 0 ]]; then
printf '%s\n' "${components[@]}"
return 0
else
log_error "无法从安装记录文件中提取组件信息"
return 1
fi
fi
}
# 主函数
main() {
echo "==========================================" >&2
echo " 整体健康检查脚本" >&2
echo "==========================================" >&2
echo >&2
# 记录健康检查开始时间
local start_time=$(get_timestamp)
log_info "健康检查开始时间: $start_time"
# 创建健康状态目录
local health_dir
health_dir=$(create_health_dir)
# 从安装记录文件中读取组件信息
log_info "从安装记录文件读取组件信息: $INSTALL_RECORD_FILE"
local components_info
if ! components_info=$(read_install_record "$INSTALL_RECORD_FILE"); then
log_error "无法读取安装记录文件,健康检查终止"
exit 1
fi
# 存储所有检查结果
local all_results=()
local overall_status="health"
# 逐个检查组件
while IFS= read -r component_info; do
if [[ -n "$component_info" ]]; then
IFS=':' read -r component_name install_dir <<< "$component_info"
local check_script_path="$install_dir/check_health.sh"
local result
local component_status="healthy"
local error_msg=""
if result=$(check_component "$component_name" "$check_script_path"); then
all_results+=("$result")
else
all_results+=("$result")
overall_status="unhealth"
component_status="unhealthy"
# 从结果中提取错误信息
if command -v jq &> /dev/null; then
error_msg=$(echo "$result" | jq -r '.reason // ""' 2>/dev/null || echo "")
else
# 简单的文本解析提取错误信息
if [[ "$result" =~ \"reason\":[[:space:]]*\"([^\"]+)\" ]]; then
error_msg="${BASH_REMATCH[1]}"
fi
fi
fi
# 写入单个模块的健康状态JSON文件
write_component_health_json "$component_name" "$component_status" "$error_msg" "$health_dir"
fi
done <<< "$components_info"
# 记录健康检查结束时间
local end_time=$(get_timestamp)
log_info "健康检查结束时间: $end_time"
# 构建完整的健康检查结果 JSON
local health_check_result=$(cat << EOF
{
"start_time": "$start_time",
"end_time": "$end_time",
"overall_status": "$overall_status",
"components": [
$(printf '%s,\n' "${all_results[@]}" | sed '$s/,$//')
]
}
EOF
)
# 写入健康日志文件
log_info "将健康检查结果写入日志文件: $HEALTH_LOG_FILE"
echo "$health_check_result" >> "$HEALTH_LOG_FILE"
# 输出 JSON 结果到 stdout
echo "$health_check_result"
# 显示总结到 stderr
echo >&2
echo "==========================================" >&2
echo " 健康检查总结" >&2
echo "==========================================" >&2
echo "开始时间: $start_time" >&2
echo "结束时间: $end_time" >&2
echo "整体状态: $overall_status" >&2
echo "日志文件: $HEALTH_LOG_FILE" >&2
echo >&2
if [[ "$overall_status" == "health" ]]; then
log_success "所有组件健康检查通过!"
exit 0
else
log_error "部分组件健康检查失败,请查看上述详细信息"
exit 1
fi
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,240 @@
#!/bin/bash
# 版本校验脚本
# 比较本地 LATEST_VERSION 与 FTP 的 VERSION 版本,如果不一致则更新对应版本
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数 - 输出到 stderr 避免影响函数返回值
log_info() {
echo -e "${BLUE}[INFO]${NC} $1" >&2
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1" >&2
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 动态获取当前版本目录
get_current_version_dir() {
# 查找 /opt/argus-metric/versions/ 下的最新版本目录
local versions_dir="/opt/argus-metric/versions"
if [[ -d "$versions_dir" ]]; then
# 按版本号排序,获取最新的版本目录
local latest_version_dir=$(ls -1 "$versions_dir" 2>/dev/null | sort -V | tail -1)
if [[ -n "$latest_version_dir" ]]; then
echo "$versions_dir/$latest_version_dir"
else
echo "/opt/argus-metric"
fi
else
echo "/opt/argus-metric"
fi
}
# 获取当前版本目录
CURRENT_VERSION_DIR=$(get_current_version_dir)
# LATEST_VERSION 文件在根目录
LOCAL_VERSION_FILE="/opt/argus-metric/LATEST_VERSION"
REMOTE_VERSION_URL=""
LOG_FILE="$CURRENT_VERSION_DIR/.version_check.log"
# 从环境变量或配置文件获取 FTP 服务器信息
get_ftp_config() {
# 优先从环境变量获取配置
log_info "获取 FTP 配置信息..."
# 如果环境变量中没有设置,则尝试从配置文件读取
if [[ -z "$FTP_SERVER" || -z "$FTP_USER" || -z "$FTP_PASSWORD" ]]; then
local config_file="$SCRIPT_DIR/../config/config.env"
if [[ -f "$config_file" ]]; then
log_info "从配置文件读取 FTP 配置: $config_file"
source "$config_file"
fi
else
log_info "使用环境变量中的 FTP 配置"
fi
# 设置默认值(如果环境变量和配置文件都没有设置)
FTP_SERVER="${FTP_SERVER:-localhost}"
FTP_USER="${FTP_USER:-ftpuser}"
FTP_PASSWORD="${FTP_PASSWORD:-ZGClab1234!}"
# 构建远程版本文件 URL
REMOTE_VERSION_URL="ftp://${FTP_USER}:${FTP_PASSWORD}@${FTP_SERVER}/LATEST_VERSION"
log_info "FTP 配置来源: ${FTP_CONFIG_SOURCE:-环境变量/配置文件}"
}
# 获取远程版本号
get_remote_version() {
log_info "从 FTP 服务器获取远程版本号..."
log_info "远程地址: $REMOTE_VERSION_URL"
# 先测试 FTP 连接
log_info "测试 FTP 连接..."
if curl -u "${FTP_USER}:${FTP_PASSWORD}" -sfI "ftp://${FTP_SERVER}/" >/dev/null 2>&1; then
log_success "FTP 服务器连接成功"
else
log_error "无法连接到 FTP 服务器: $FTP_SERVER"
return 1
fi
# 测试 LATEST_VERSION 文件是否存在
log_info "检查远程 LATEST_VERSION 文件是否存在..."
if curl -u "${FTP_USER}:${FTP_PASSWORD}" -sfI "ftp://${FTP_SERVER}/LATEST_VERSION" >/dev/null 2>&1; then
log_success "远程 LATEST_VERSION 文件存在"
else
log_error "远程 LATEST_VERSION 文件不存在或无法访问"
return 1
fi
# 获取远程版本号
local remote_version
if remote_version=$(curl -u "${FTP_USER}:${FTP_PASSWORD}" -sfL "ftp://${FTP_SERVER}/LATEST_VERSION" 2>/dev/null | tr -d '[:space:]'); then
if [[ -n "$remote_version" ]]; then
log_success "获取到远程版本号: $remote_version"
echo "$remote_version"
else
log_error "远程版本号为空"
return 1
fi
else
log_error "获取远程版本号失败"
return 1
fi
}
# 获取本地版本号
get_local_version() {
if [[ -f "$LOCAL_VERSION_FILE" ]]; then
local local_version=$(cat "$LOCAL_VERSION_FILE" 2>/dev/null | tr -d '[:space:]')
if [[ -n "$local_version" ]]; then
log_info "本地版本号: $local_version"
echo "$local_version"
else
log_warning "本地版本文件为空"
echo ""
fi
else
log_warning "本地版本文件不存在: $LOCAL_VERSION_FILE"
echo ""
fi
}
# 更新到新版本
update_to_version() {
local new_version="$1"
local temp_dir="/tmp/argus-update-$$"
local setup_script="$temp_dir/setup.sh"
log_info "开始更新到版本: $new_version"
# 创建临时目录
mkdir -p "$temp_dir"
# 下载最新的 setup.sh
log_info "从 FTP 服务器下载最新的安装脚本..."
local setup_url="ftp://${FTP_USER}:${FTP_PASSWORD}@${FTP_SERVER}/setup.sh"
if curl -fsS "$setup_url" -o "$setup_script"; then
log_success "安装脚本下载完成"
else
log_error "下载安装脚本失败: $setup_url"
rm -rf "$temp_dir"
return 1
fi
# 添加执行权限
chmod +x "$setup_script"
# 执行安装脚本
log_info "执行安装脚本进行版本更新..."
if "$setup_script" --server "$FTP_SERVER" --user "$FTP_USER" --password "$FTP_PASSWORD" --version "$new_version"; then
log_success "版本更新完成: $new_version"
rm -rf "$temp_dir"
return 0
else
log_error "版本更新失败: $new_version"
rm -rf "$temp_dir"
return 1
fi
}
# 记录检查日志
log_check() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" >> "$LOG_FILE"
}
# 主函数
main() {
log_info "开始版本校验检查..."
log_check "版本校验检查开始"
# 确保系统目录存在
mkdir -p "/opt/argus-metric"
mkdir -p "$CURRENT_VERSION_DIR"
log_info "当前版本目录: $CURRENT_VERSION_DIR"
# 获取 FTP 配置
get_ftp_config
# 获取本地版本号
local local_version
local_version=$(get_local_version)
# 获取远程版本号
local remote_version
if ! remote_version=$(get_remote_version); then
log_error "无法获取远程版本号,跳过本次检查"
log_check "版本校验失败:无法获取远程版本号"
exit 1
fi
# 比较版本号
if [[ "$local_version" == "$remote_version" ]]; then
log_info "版本一致,无需更新 (本地: $local_version, 远程: $remote_version)"
log_check "版本校验完成:版本一致 ($local_version)"
else
log_info "检测到版本不一致 (本地: $local_version, 远程: $remote_version)"
log_check "检测到版本不一致:本地($local_version) -> 远程($remote_version)"
# 更新到新版本
if update_to_version "$remote_version"; then
log_success "版本更新成功: $local_version -> $remote_version"
log_check "版本更新成功:$local_version -> $remote_version"
else
log_error "版本更新失败"
log_check "版本更新失败:$local_version -> $remote_version"
exit 1
fi
fi
log_success "版本校验检查完成"
log_check "版本校验检查完成"
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,903 @@
#!/bin/bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 配置变量
INSTALL_DIR="${1:-$(pwd)}" # 使用第一个参数作为安装目录,如果没有参数则使用当前目录
TEMP_DIR="/tmp/metrics-install-$$"
VERSION_FILE="version.json"
# 加载配置文件
load_config() {
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local config_file="$script_dir/config.env"
if [[ -f "$config_file" ]]; then
log_info "加载配置文件: $config_file"
# 导出配置文件中的环境变量
set -a # 自动导出所有变量
source "$config_file"
set +a # 关闭自动导出
log_success "配置文件加载完成"
else
log_warning "配置文件不存在: $config_file,使用默认配置"
fi
}
# 复制配置文件到安装目录
copy_config_files() {
log_info "复制配置文件到安装目录..."
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local source_config="$script_dir/../config/config.env"
local target_config="$INSTALL_DIR/config.env"
if [[ -f "$source_config" ]]; then
# 检查源文件和目标文件是否是同一个文件
if [[ "$source_config" == "$target_config" ]]; then
log_info "配置文件已在目标位置,跳过复制"
log_success "配置文件已存在: $target_config"
else
if cp "$source_config" "$target_config"; then
log_success "配置文件复制完成: $target_config"
else
log_error "配置文件复制失败"
return 1
fi
fi
else
log_warning "源配置文件不存在: $source_config"
fi
# 复制版本校验脚本
log_info "复制版本校验脚本到安装目录..."
local target_check_version="$INSTALL_DIR/check_version.sh"
# 检查目标文件是否已存在(从 artifact 包中解压出来的)
if [[ -f "$target_check_version" ]]; then
log_info "版本校验脚本已存在,设置执行权限..."
chmod +x "$target_check_version"
log_success "版本校验脚本权限设置完成: $target_check_version"
else
log_warning "版本校验脚本不存在: $target_check_version"
log_info "请确保 check_version.sh 已包含在 artifact 包中"
fi
}
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "此脚本需要 root 权限运行"
log_info "请使用: sudo $0 [安装目录]"
log_info "如果不指定安装目录,将使用当前目录: $(pwd)"
exit 1
fi
}
# 检查系统要求
check_system() {
log_info "检查系统要求..."
# 检查操作系统
if [[ ! -f /etc/os-release ]]; then
log_error "无法检测操作系统版本"
exit 1
fi
source /etc/os-release
log_info "检测到操作系统: $NAME $VERSION"
# 检查系统架构
arch=$(uname -m)
log_info "系统架构: $arch"
# 检查磁盘空间
available_space=$(df / | awk 'NR==2 {print $4}')
if [[ $available_space -lt 10485760 ]]; then # 10GB in KB
log_warning "可用磁盘空间不足 10GB,当前可用: $(($available_space / 1024 / 1024))GB"
fi
# 检查内存
total_mem=$(free -m | awk 'NR==2{print $2}')
if [[ $total_mem -lt 4096 ]]; then # 4GB
log_warning "系统内存不足 4GB,当前: ${total_mem}MB"
fi
}
# 查找版本文件
find_version_file() {
log_info "查找版本信息文件..."
# 在当前目录查找
if [[ -f "$VERSION_FILE" ]]; then
VERSION_FILE_PATH="$(pwd)/$VERSION_FILE"
log_success "找到版本文件: $VERSION_FILE"
return 0
fi
# 在 artifact 目录查找
for version_dir in artifact/*/; do
if [[ -f "${version_dir}${VERSION_FILE}" ]]; then
VERSION_FILE_PATH="$(cd "$(dirname "${version_dir}${VERSION_FILE}")" && pwd)/$(basename "${version_dir}${VERSION_FILE}")"
log_success "找到版本文件: $VERSION_FILE_PATH"
return 0
fi
done
log_error "未找到版本信息文件 $VERSION_FILE"
exit 1
}
# 解析版本信息
parse_version_info() {
log_info "解析版本信息..."
if [[ ! -f "$VERSION_FILE_PATH" ]]; then
log_error "版本文件不存在: $VERSION_FILE_PATH"
exit 1
fi
# 使用 jq 解析 JSON(如果可用)
if command -v jq &> /dev/null; then
# 验证JSON文件格式
if ! jq empty "$VERSION_FILE_PATH" 2>/dev/null; then
log_error "JSON文件格式错误,请检查 $VERSION_FILE_PATH"
exit 1
fi
VERSION=$(jq -r '.version' "$VERSION_FILE_PATH")
BUILD_TIME=$(jq -r '.build_time' "$VERSION_FILE_PATH")
# 解析 artifact_list
if jq -e '.artifact_list' "$VERSION_FILE_PATH" > /dev/null 2>&1; then
jq -r '.artifact_list | to_entries[] | "\(.key):\(.value)"' "$VERSION_FILE_PATH" > "$TEMP_DIR/components.txt"
else
log_error "version.json 中缺少 artifact_list 字段"
exit 1
fi
# 解析 checksums
if jq -e '.checksums' "$VERSION_FILE_PATH" > /dev/null 2>&1; then
jq -r '.checksums | to_entries[] | "\(.key):\(.value)"' "$VERSION_FILE_PATH" > "$TEMP_DIR/checksums.txt"
else
log_error "version.json 中缺少 checksums 字段"
exit 1
fi
# 解析 install_order(现在包含完整的文件名)
if jq -e '.install_order' "$VERSION_FILE_PATH" > /dev/null 2>&1; then
jq -r '.install_order[]' "$VERSION_FILE_PATH" > "$TEMP_DIR/install_order.txt"
else
log_error "version.json 中缺少 install_order 字段"
exit 1
fi
else
log_warning "jq 未安装,使用简单的 JSON 解析"
# 简单的 JSON 解析
VERSION=$(grep '"version"' "$VERSION_FILE_PATH" | sed 's/.*"version": *"\([^"]*\)".*/\1/')
BUILD_TIME=$(grep '"build_time"' "$VERSION_FILE_PATH" | sed 's/.*"build_time": *"\([^"]*\)".*/\1/')
# 解析 artifact_list
grep -A 100 '"artifact_list"' "$VERSION_FILE_PATH" | grep -E '^\s*"[^"]+":\s*"[^"]+"' | while read line; do
component=$(echo "$line" | sed 's/.*"\([^"]*\)":\s*"[^"]*".*/\1/')
version=$(echo "$line" | sed 's/.*"[^"]*":\s*"\([^"]*\)".*/\1/')
echo "$component:$version" >> "$TEMP_DIR/components.txt"
done
# 解析 checksums
grep -A 100 '"checksums"' "$VERSION_FILE_PATH" | grep -E '^\s*"[^"]+":\s*"[^"]+"' | while read line; do
component=$(echo "$line" | sed 's/.*"\([^"]*\)":\s*"[^"]*".*/\1/')
checksum=$(echo "$line" | sed 's/.*"[^"]*":\s*"\([^"]*\)".*/\1/')
echo "$component:$checksum" >> "$TEMP_DIR/checksums.txt"
done
# 解析 install_order
grep -A 100 '"install_order"' "$VERSION_FILE_PATH" | grep -E '^\s*"[^"]+"' | while read line; do
component=$(echo "$line" | sed 's/.*"\([^"]*\)".*/\1/')
echo "$component" >> "$TEMP_DIR/install_order.txt"
done
# 验证解析结果
if [[ ! -f "$TEMP_DIR/components.txt" || ! -s "$TEMP_DIR/components.txt" ]]; then
log_error "无法解析 artifact_list,请检查 version.json 格式"
exit 1
fi
if [[ ! -f "$TEMP_DIR/checksums.txt" || ! -s "$TEMP_DIR/checksums.txt" ]]; then
log_error "无法解析 checksums,请检查 version.json 格式"
exit 1
fi
if [[ ! -f "$TEMP_DIR/install_order.txt" || ! -s "$TEMP_DIR/install_order.txt" ]]; then
log_error "无法解析 install_order,请检查 version.json 格式"
exit 1
fi
fi
log_success "版本信息解析完成"
log_info " 版本: $VERSION"
log_info " 构建时间: $BUILD_TIME"
component_count=0
if [[ -f "$TEMP_DIR/components.txt" ]]; then
component_count=$(wc -l < "$TEMP_DIR/components.txt")
log_info " 组件数量: $component_count"
log_info " 组件列表:"
while IFS= read -r line; do
component=$(echo "$line" | cut -d':' -f1)
version=$(echo "$line" | cut -d':' -f2)
log_info " - $component v$version"
done < "$TEMP_DIR/components.txt"
else
log_error "components.txt 文件不存在"
exit 1
fi
}
# 验证文件完整性
verify_checksums() {
log_info "验证文件完整性..."
artifact_dir=$(dirname "$VERSION_FILE_PATH")
log_info "Artifact 目录: $artifact_dir"
failed_verification=0
if [[ -f "$TEMP_DIR/checksums.txt" ]]; then
while IFS= read -r line; do
component=$(echo "$line" | cut -d':' -f1)
expected_checksum=$(echo "$line" | cut -d':' -f2-)
# 查找匹配的 tar 文件
actual_file=""
for file in "$artifact_dir/${component}-"*.tar.gz; do
if [[ -f "$file" ]]; then
actual_file="$file"
break
fi
done
if [[ -z "$actual_file" ]]; then
log_error "找不到组件文件: $component"
failed_verification=1
continue
fi
# 计算实际校验和
actual_checksum="sha256:$(sha256sum "$actual_file" | cut -d' ' -f1)"
if [[ "$actual_checksum" == "$expected_checksum" ]]; then
log_success " $component: 校验通过"
else
log_error " $component: 校验失败"
log_error " 期望: $expected_checksum"
log_error " 实际: $actual_checksum"
failed_verification=1
fi
done < "$TEMP_DIR/checksums.txt"
fi
if [[ $failed_verification -eq 1 ]]; then
log_error "文件完整性验证失败"
exit 1
fi
log_success "所有文件校验通过"
}
# 创建安装目录
create_install_dirs() {
log_info "创建安装目录..."
mkdir -p "$INSTALL_DIR"
mkdir -p "$TEMP_DIR"
log_success "安装目录创建完成: $INSTALL_DIR"
}
# 安装系统依赖包
install_system_deps() {
log_info "检查系统依赖包..."
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local deps_dir="$script_dir/deps"
# 检查deps目录是否存在
if [[ ! -d "$deps_dir" ]]; then
log_info "deps 目录不存在,跳过系统依赖包安装"
return 0
fi
# 检查是否有tar.gz文件
local deps_count=$(find "$deps_dir" -name "*.tar.gz" | wc -l)
if [[ $deps_count -eq 0 ]]; then
log_info "deps 目录中没有 tar.gz 文件,跳过系统依赖包安装"
return 0
fi
log_info "找到 $deps_count 个系统依赖包,开始安装..."
# 创建临时目录用于解压依赖包
local deps_temp_dir="$TEMP_DIR/deps"
mkdir -p "$deps_temp_dir"
# 处理每个tar.gz文件
find "$deps_dir" -name "*.tar.gz" | while read tar_file; do
local tar_basename=$(basename "$tar_file")
local extract_name="${tar_basename%.tar.gz}"
log_info "处理依赖包: $tar_basename"
# 解压到临时目录
local extract_dir="$deps_temp_dir/$extract_name"
mkdir -p "$extract_dir"
if tar -xzf "$tar_file" -C "$extract_dir" 2>/dev/null; then
log_success " $tar_basename 解压完成"
else
log_error " $tar_basename 解压失败"
continue
fi
# 进入解压目录,查找deb包
cd "$extract_dir"
local deb_count=$(find . -name "*.deb" | wc -l)
if [[ $deb_count -gt 0 ]]; then
log_info " 找到 $deb_count 个 deb 包,开始安装..."
# 1. 先尝试安装所有deb包
log_info " 第1步:批量安装deb包..."
if dpkg -i *.deb 2>/dev/null; then
log_success " 所有deb包安装成功"
else
log_warning " 部分deb包安装失败,可能存在依赖问题"
# 2. 使用apt-get修复依赖
log_info " 第2步:修复依赖关系..."
if apt-get install -f -y; then
log_success " 依赖关系修复完成"
else
log_error " 依赖关系修复失败"
# 继续处理其他包,不退出
fi
fi
else
log_info " $tar_basename 中没有找到deb包,跳过"
fi
# 返回到依赖临时目录
cd "$deps_temp_dir"
done
# 检查并启动 cron 服务
start_cron_service
log_success "系统依赖包安装完成"
}
# 启动 cron 服务
start_cron_service() {
log_info "检查并启动 cron 服务..."
# 检查 cron 是否已经在运行
if pgrep -x "cron" > /dev/null; then
log_success "cron 服务已在运行"
return 0
fi
# 检查 /usr/sbin/cron 是否存在
if [[ ! -f "/usr/sbin/cron" ]]; then
log_warning "cron 可执行文件不存在,跳过启动"
return 1
fi
# 启动 cron 服务
log_info "启动 cron 服务..."
if /usr/sbin/cron start 2>/dev/null || /usr/sbin/cron 2>/dev/null; then
log_success "cron 服务启动成功"
sleep 2
if pgrep -x "cron" > /dev/null; then
log_success "cron 服务运行正常"
else
log_warning "cron 服务可能未正常启动"
fi
else
log_error "cron 服务启动失败"
return 1
fi
}
# 安装组件
install_components() {
log_info "开始安装组件..."
artifact_dir=$(dirname "$VERSION_FILE_PATH")
log_info "Artifact 目录: $artifact_dir"
install_count=0
total_count=0
if [[ -f "$TEMP_DIR/install_order.txt" ]]; then
total_count=$(wc -l < "$TEMP_DIR/install_order.txt")
fi
if [[ -f "$TEMP_DIR/install_order.txt" ]]; then
while IFS= read -r filename; do
install_count=$((install_count + 1))
# 从文件名中提取组件名(去掉时间戳后缀)
component=$(echo "$filename" | sed 's/-[0-9]\{8\}-[0-9]\{6\}\.tar\.gz$//')
log_info "[$install_count/$total_count] 安装 $component..."
log_info " 文件名: $filename"
# 直接使用完整的文件名
tar_file="$artifact_dir/$filename"
if [[ ! -f "$tar_file" ]]; then
log_error "找不到组件文件: $filename"
log_info " 期望路径: $tar_file"
log_info " 当前目录: $(pwd)"
log_info " 目录内容:"
ls -la "$artifact_dir" | while read line; do
log_info " $line"
done
exit 1
fi
log_info " 找到文件: $tar_file"
# 解压到临时目录
component_temp_dir="$TEMP_DIR/$component"
mkdir -p "$component_temp_dir"
if tar -xzf "$tar_file" -C "$component_temp_dir" 2>/dev/null; then
log_success " $component 解压完成"
else
log_error " $component 解压失败"
exit 1
fi
# 查找解压后的目录
extracted_dir=""
for dir in "$component_temp_dir"/*; do
if [[ -d "$dir" ]]; then
extracted_dir="$dir"
break
fi
done
if [[ -z "$extracted_dir" ]]; then
log_error " $component 解压后未找到目录"
exit 1
fi
# 执行安装脚本
if [[ -f "$extracted_dir/install.sh" ]]; then
log_info " 执行 $component 安装脚本..."
if (cd "$extracted_dir" && ./install.sh "$INSTALL_DIR"); then
log_success " $component 安装完成"
else
log_error " $component 安装失败"
exit 1
fi
else
log_error " $component 缺少 install.sh 文件"
exit 1
fi
# 将解压后的目录移动到安装目录,保留组件目录
component_install_dir="$INSTALL_DIR/$component"
# 简化安装逻辑:直接删除旧目录,不进行备份
if [[ -d "$component_install_dir" ]]; then
log_info " 组件目录已存在,删除旧版本: $component_install_dir"
rm -rf "$component_install_dir"
# log_info " 组件目录已存在,备份后更新: $component_install_dir"
# mv "$component_install_dir" "${component_install_dir}.backup.$(date +%Y%m%d_%H%M%S)"
fi
mv "$extracted_dir" "$component_install_dir"
log_success " 组件目录已保存: $component_install_dir"
# 清理临时文件
rm -rf "$component_temp_dir"
done < "$TEMP_DIR/install_order.txt"
fi
log_success "所有组件安装完成"
}
# 创建安装记录
create_install_record() {
log_info "创建安装记录..."
# 等待一段时间确保所有进程都已启动
log_info "等待进程启动..."
sleep 3
local install_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local install_record_file="$INSTALL_DIR/.install_record"
# 创建 JSON 格式的安装记录
cat > "$install_record_file" << EOF
{
"version": "$VERSION",
"build_time": "$BUILD_TIME",
"install_time": "$install_time",
"install_dir": "$INSTALL_DIR",
"install_pid": $$,
"components": {
EOF
# 添加组件信息
local first_component=true
if [[ -f "$TEMP_DIR/components.txt" ]]; then
while IFS= read -r line; do
component=$(echo "$line" | cut -d':' -f1)
version=$(echo "$line" | cut -d':' -f2)
# 获取组件的进程信息
local component_pid=""
# 根据组件名查找进程,使用多种方法确保能找到PID
case "$component" in
"node-exporter")
# 尝试多种方式查找node_exporter进程
component_pid=$(pgrep -f "node_exporter" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(pgrep -f "node-exporter" | head -1)
fi
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "node_exporter" | awk '{print $2}' | head -1)
fi
;;
"dcgm-exporter")
# 查找dcgm-exporter进程
component_pid=$(pgrep -f "dcgm-exporter" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(pgrep -f "dcgm_exporter" | head -1)
fi
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "dcgm-exporter" | awk '{print $2}' | head -1)
fi
;;
"fluent-bit")
# 查找fluent-bit进程
component_pid=$(pgrep -f "fluent-bit" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(pgrep -f "fluent_bit" | head -1)
fi
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "fluent-bit" | awk '{print $2}' | head -1)
fi
;;
"argus-agent")
# 查找argus-agent进程
component_pid=$(pgrep -f "argus-agent" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "argus-agent" | awk '{print $2}' | head -1)
fi
;;
esac
# 记录找到的PID信息
if [[ -n "$component_pid" ]]; then
log_info " 找到 $component 进程 PID: $component_pid"
else
log_warning " 未找到 $component 进程"
fi
# 添加逗号分隔符
if [[ "$first_component" == "true" ]]; then
first_component=false
else
echo "," >> "$install_record_file"
fi
# 添加组件信息
cat >> "$install_record_file" << EOF
"$component": {
"version": "$version",
"pid": "$component_pid",
"install_dir": "$INSTALL_DIR/$component"
}
EOF
done < "$TEMP_DIR/components.txt"
fi
# 结束 JSON
cat >> "$install_record_file" << EOF
}
}
EOF
log_success "安装记录已创建: $install_record_file"
}
# 设置健康检查定时任务
setup_health_check_cron() {
log_info "设置健康检查定时任务..."
# 直接使用当前安装目录,不依赖current软链接
# INSTALL_DIR 是 /opt/argus-metric/versions/1.34.0
local check_health_script="$INSTALL_DIR/check_health.sh"
# 检查健康检查脚本是否存在
if [[ ! -f "$check_health_script" ]]; then
log_error "健康检查脚本不存在: $check_health_script"
return 1
fi
# 确保脚本有执行权限
chmod +x "$check_health_script"
# 创建临时crontab文件
local temp_cron="/tmp/crontab_$$"
# 获取当前用户的crontab(如果存在)
crontab -l 2>/dev/null > "$temp_cron" || touch "$temp_cron"
# 检查并删除旧的健康检查任务
if grep -q "check_health.sh" "$temp_cron"; then
log_info "发现旧的健康检查定时任务,正在更新..."
# 删除所有包含check_health.sh的行
grep -v "check_health.sh" "$temp_cron" > "$temp_cron.new"
mv "$temp_cron.new" "$temp_cron"
log_info "旧的健康检查定时任务已删除"
fi
# 添加新的定时任务(每5分钟执行一次)
echo "# Argus-Metrics 健康检查定时任务" >> "$temp_cron"
echo "*/5 * * * * $check_health_script >> $INSTALL_DIR/.health_cron.log 2>&1" >> "$temp_cron"
# 安装新的crontab
if crontab "$temp_cron"; then
log_success "健康检查定时任务设置成功"
log_info " 执行频率: 每5分钟"
log_info " 日志文件: $INSTALL_DIR/.health_cron.log"
log_info " 查看定时任务: crontab -l"
log_info " 删除定时任务: crontab -e"
else
log_error "健康检查定时任务设置失败"
rm -f "$temp_cron"
return 1
fi
# 清理临时文件
rm -f "$temp_cron"
log_info "健康检查通过crontab自动执行"
}
# 设置 DNS 同步定时任务
setup_dns_sync_cron() {
log_info "设置 DNS 同步定时任务..."
# 使用当前版本目录中的 DNS 同步脚本
local sync_dns_script="$INSTALL_DIR/sync_dns.sh"
# 检查 DNS 同步脚本是否存在
if [[ ! -f "$sync_dns_script" ]]; then
log_warning "DNS 同步脚本不存在: $sync_dns_script"
log_warning "跳过 DNS 同步定时任务设置"
return 0
fi
# 确保脚本有执行权限
chmod +x "$sync_dns_script"
# 创建临时crontab文件
local temp_cron="/tmp/crontab_$$"
# 获取当前用户的crontab(如果存在)
crontab -l 2>/dev/null > "$temp_cron" || touch "$temp_cron"
# 检查并删除旧的 DNS 同步任务
if grep -q "sync_dns.sh" "$temp_cron"; then
log_info "发现旧的 DNS 同步定时任务,正在更新..."
# 删除所有包含sync_dns.sh的行
grep -v "sync_dns.sh" "$temp_cron" > "$temp_cron.new"
mv "$temp_cron.new" "$temp_cron"
log_info "旧的 DNS 同步定时任务已删除"
fi
# 添加新的定时任务(每30秒执行一次)
# 直接使用版本目录中的 DNS 同步脚本
echo "# Argus-Metrics DNS 同步定时任务" >> "$temp_cron"
echo "* * * * * $sync_dns_script >> $INSTALL_DIR/.dns_sync.log 2>&1" >> "$temp_cron"
echo "* * * * * sleep 30; $sync_dns_script >> $INSTALL_DIR/.dns_sync.log 2>&1" >> "$temp_cron"
# 安装新的crontab
if crontab "$temp_cron"; then
log_success "DNS 同步定时任务设置成功"
log_info " 执行频率: 每30秒"
log_info " 日志文件: $INSTALL_DIR/.dns_sync.log"
log_info " 查看定时任务: crontab -l"
log_info " 删除定时任务: crontab -e"
else
log_error "DNS 同步定时任务设置失败"
rm -f "$temp_cron"
return 1
fi
# 清理临时文件
rm -f "$temp_cron"
log_info "DNS 同步通过crontab自动执行"
}
# 设置版本校验定时任务
setup_version_check_cron() {
log_info "设置版本校验定时任务..."
# 使用当前版本目录中的版本校验脚本
local check_version_script="$INSTALL_DIR/check_version.sh"
# 检查脚本是否存在
if [[ ! -f "$check_version_script" ]]; then
log_warning "版本校验脚本不存在: $check_version_script"
log_info "跳过版本校验定时任务设置"
return 0
fi
# 确保脚本可执行
chmod +x "$check_version_script"
# 创建临时crontab文件
local temp_cron="/tmp/crontab_$$"
crontab -l > "$temp_cron" 2>/dev/null || touch "$temp_cron"
# 检查是否已存在版本校验定时任务
if grep -q "check_version.sh" "$temp_cron"; then
log_info "发现旧的版本校验定时任务,正在更新..."
# 删除所有包含check_version.sh的行
grep -v "check_version.sh" "$temp_cron" > "$temp_cron.new"
mv "$temp_cron.new" "$temp_cron"
log_info "旧的版本校验定时任务已删除"
fi
# 添加新的定时任务(每30分钟执行一次)
echo "# Argus-Metrics 版本校验定时任务" >> "$temp_cron"
echo "*/1 * * * * $check_version_script >> $INSTALL_DIR/.version_check.log 2>&1" >> "$temp_cron"
# 安装新的crontab
if crontab "$temp_cron"; then
log_success "版本校验定时任务设置成功"
log_info " 执行频率: 每1分钟"
log_info " 日志文件: $INSTALL_DIR/.version_check.log"
log_info " 查看定时任务: crontab -l"
log_info " 删除定时任务: crontab -e"
else
log_error "版本校验定时任务设置失败"
rm -f "$temp_cron"
return 1
fi
# 清理临时文件
rm -f "$temp_cron"
log_info "版本校验通过crontab自动执行"
}
# 设置自动重启定时任务
setup_restart_cron() {
log_info "设置自动重启定时任务..."
# 使用当前版本目录中的重启脚本
local restart_script="$INSTALL_DIR/restart_unhealthy.sh"
# 检查脚本是否存在
if [[ ! -f "$restart_script" ]]; then
log_warning "重启脚本不存在: $restart_script"
log_info "跳过自动重启定时任务设置"
return 0
fi
# 确保脚本可执行
chmod +x "$restart_script"
# 创建临时crontab文件
local temp_cron="/tmp/crontab_$$"
crontab -l > "$temp_cron" 2>/dev/null || touch "$temp_cron"
# 检查是否已存在自动重启定时任务
if grep -q "restart_unhealthy.sh" "$temp_cron"; then
log_info "发现旧的自动重启定时任务,正在更新..."
# 删除所有包含restart_unhealthy.sh的行
grep -v "restart_unhealthy.sh" "$temp_cron" > "$temp_cron.new"
mv "$temp_cron.new" "$temp_cron"
log_info "旧的自动重启定时任务已删除"
fi
# 添加新的定时任务(每2分钟执行一次)
echo "# Argus-Metrics 自动重启定时任务" >> "$temp_cron"
echo "*/2 * * * * $restart_script >> $INSTALL_DIR/.restart.log 2>&1" >> "$temp_cron"
# 安装新的crontab
if crontab "$temp_cron"; then
log_success "自动重启定时任务设置成功"
log_info " 执行频率: 每2分钟"
log_info " 日志文件: $INSTALL_DIR/.restart.log"
log_info " 查看定时任务: crontab -l"
log_info " 删除定时任务: crontab -e"
else
log_error "自动重启定时任务设置失败"
rm -f "$temp_cron"
return 1
fi
# 清理临时文件
rm -f "$temp_cron"
log_info "自动重启检查通过crontab自动执行"
}
# 显示安装信息
show_install_info() {
log_success "Argus-Metrics All-in-One 安装完成!"
}
cleanup() {
if [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
# 主函数
main() {
echo "=========================================="
echo " Argus-Metrics All-in-One 安装脚本 v1.0"
echo "=========================================="
echo
# 加载配置文件
load_config
log_info "安装目录: $INSTALL_DIR"
echo
check_root
check_system
find_version_file
create_install_dirs
parse_version_info
verify_checksums
install_system_deps
install_components
copy_config_files
create_install_record
setup_health_check_cron
setup_dns_sync_cron
setup_version_check_cron
setup_restart_cron
show_install_info
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,474 @@
#!/bin/bash
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 显示帮助信息
show_help() {
echo "AIOps All-in-One 打包脚本"
echo
echo "用法: $0 [选项]"
echo
echo "选项:"
echo " --force 强制重新打包,即使版本已存在"
echo " --help 显示此帮助信息"
echo
echo "示例:"
echo " $0 # 正常打包,跳过已存在的版本"
echo " $0 --force # 强制重新打包"
echo
}
# 解析命令行参数
FORCE_PACKAGE=false
if [[ "$1" == "--force" ]]; then
FORCE_PACKAGE=true
log_info "强制重新打包模式"
elif [[ "$1" == "--help" || "$1" == "-h" ]]; then
show_help
exit 0
fi
# 获取当前目录和版本
CURRENT_DIR=$(pwd)
VERSION=$(cat config/VERSION 2>/dev/null || echo "1.0.0")
ARTIFACT_DIR="artifact/$VERSION"
log_info "开始打包 AIOps All-in-One 安装包 v$VERSION"
# 检查必要文件
log_info "检查必要文件..."
if [[ ! -f "config/VERSION" ]]; then
log_error "VERSION 文件不存在"
exit 1
fi
if [[ ! -f "config/checklist" ]]; then
log_error "checklist 文件不存在"
exit 1
fi
# 检查是否已存在该版本
if [[ -d "$ARTIFACT_DIR" && "$FORCE_PACKAGE" == "false" ]]; then
log_info "检查版本 $VERSION 是否已存在..."
# 检查 version.json 是否存在
if [[ -f "$ARTIFACT_DIR/version.json" ]]; then
log_info "找到已存在的版本信息文件"
# 检查是否所有组件文件都存在
missing_files=0
existing_components=0
# 解析已存在的 version.json 来检查文件
if command -v jq &> /dev/null; then
# 使用 jq 解析
while IFS= read -r component; do
existing_components=$((existing_components + 1))
# 查找对应的 tar 文件
found_file=false
for file in "$ARTIFACT_DIR/${component}-"*.tar.gz; do
if [[ -f "$file" ]]; then
found_file=true
break
fi
done
if [[ "$found_file" == "false" ]]; then
missing_files=$((missing_files + 1))
log_warning " 缺少文件: $component"
fi
done < <(jq -r '.artifact_list | keys[]' "$ARTIFACT_DIR/version.json" 2>/dev/null)
else
# 简单的文件检查
for file in "$ARTIFACT_DIR"/*.tar.gz; do
if [[ -f "$file" ]]; then
existing_components=$((existing_components + 1))
fi
done
fi
# 如果所有文件都存在,则跳过打包
if [[ $missing_files -eq 0 && $existing_components -gt 0 ]]; then
log_success "版本 $VERSION 已完整打包,跳过重复打包"
echo
echo "现有文件:"
ls -la "$ARTIFACT_DIR"
echo
echo "如需强制重新打包,请删除目录: rm -rf $ARTIFACT_DIR"
echo "或使用: ./package.sh --force"
exit 0
else
log_warning "版本 $VERSION 存在但不完整,将重新打包"
log_info " 现有组件: $existing_components"
log_info " 缺少文件: $missing_files"
fi
else
log_warning "版本目录存在但缺少 version.json,将重新打包"
fi
fi
# 创建 artifact 目录
mkdir -p "$ARTIFACT_DIR"
log_info "创建输出目录: $ARTIFACT_DIR"
# 创建临时文件存储数据
TEMP_DIR=$(mktemp -d)
COMPONENTS_FILE="$TEMP_DIR/components.txt"
VERSIONS_FILE="$TEMP_DIR/versions.txt"
DEPENDENCIES_FILE="$TEMP_DIR/dependencies.txt"
INSTALL_ORDER_FILE="$TEMP_DIR/install_order.txt"
CHECKSUMS_FILE="$TEMP_DIR/checksums.txt"
ARTIFACT_LIST_FILE="$TEMP_DIR/artifact_list.txt"
# 解析 checklist 文件
log_info "解析组件清单..."
line_num=0
component_count=0
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
line_num=$((line_num + 1))
# 解析行: 组件名 目录路径 版本 [依赖组件] [安装顺序]
read -r component component_path version dep_component order <<< "$line"
if [[ -z "$component" || -z "$component_path" || -z "$version" ]]; then
log_warning "跳过无效行 $line_num: $line"
continue
fi
# 存储组件信息
echo "$component" >> "$COMPONENTS_FILE"
echo "$component:$version" >> "$VERSIONS_FILE"
echo "$component:$component_path" >> "$TEMP_DIR/component_paths.txt"
if [[ -n "$dep_component" && "$dep_component" != "$component" ]]; then
echo "$component:$dep_component" >> "$DEPENDENCIES_FILE"
fi
if [[ -n "$order" && "$order" =~ ^[0-9]+$ ]]; then
echo "$order:$component" >> "$INSTALL_ORDER_FILE"
else
# 如果没有指定顺序,按解析顺序分配
echo "$line_num:$component" >> "$INSTALL_ORDER_FILE"
fi
component_count=$((component_count + 1))
log_info " - $component v$version"
done < config/checklist
if [[ $component_count -eq 0 ]]; then
log_error "没有找到有效的组件"
rm -rf "$TEMP_DIR"
exit 1
fi
log_success "找到 $component_count 个组件"
# 检查组件目录是否存在
log_info "检查组件目录..."
missing_components=()
while IFS= read -r component; do
# 获取组件路径
component_path=$(grep "^$component:" "$TEMP_DIR/component_paths.txt" | cut -d':' -f2-)
if [[ -z "$component_path" ]]; then
log_error "未找到组件 $component 的路径配置"
log_info "请检查 component_paths.txt 文件或添加路径配置"
exit 1
fi
if [[ ! -d "$component_path" ]]; then
missing_components+=("$component:$component_path")
fi
done < "$COMPONENTS_FILE"
if [[ ${#missing_components[@]} -gt 0 ]]; then
log_error "以下组件目录不存在:"
for component_path in "${missing_components[@]}"; do
echo " - $component_path"
done
rm -rf "$TEMP_DIR"
exit 1
fi
# 打包各个组件
log_info "开始打包组件..."
while IFS= read -r component; do
# 获取组件版本和路径
version=$(grep "^$component:" "$VERSIONS_FILE" | cut -d':' -f2)
component_path=$(grep "^$component:" "$TEMP_DIR/component_paths.txt" | cut -d':' -f2-)
if [[ -z "$component_path" ]]; then
log_error "未找到组件 $component 的路径配置"
log_info "请检查 component_paths.txt 文件或添加路径配置"
exit 1
fi
log_info "打包 $component v$version..."
log_info " 组件路径: $component_path"
# 进入组件目录
cd "$component_path"
# 检查组件是否有 package.sh
if [[ ! -f "package.sh" ]]; then
log_error "$component 缺少 package.sh 文件"
cd "$CURRENT_DIR"
rm -rf "$TEMP_DIR"
exit 1
fi
# 执行组件的打包脚本
if ./package.sh; then
# 查找生成的 tar 包
tar_file=$(find . -name "*.tar.gz" -type f | head -1)
if [[ -n "$tar_file" ]]; then
# 移动到 artifact 目录
mv "$tar_file" "$CURRENT_DIR/$ARTIFACT_DIR/"
tar_filename=$(basename "$tar_file")
# 计算校验和
checksum=$(sha256sum "$CURRENT_DIR/$ARTIFACT_DIR/$tar_filename" | cut -d' ' -f1)
echo "$component:sha256:$checksum" >> "$CHECKSUMS_FILE"
echo "$component:$version" >> "$ARTIFACT_LIST_FILE"
# 将完整的文件名存储到安装顺序文件中
echo "$tar_filename" >> "$TEMP_DIR/install_order_files.txt"
log_success " $component 打包完成: $tar_filename"
else
log_error "$component 打包失败,未找到生成的 tar 包"
cd "$CURRENT_DIR"
rm -rf "$TEMP_DIR"
exit 1
fi
else
log_error "$component 打包失败"
cd "$CURRENT_DIR"
rm -rf "$TEMP_DIR"
exit 1
fi
# 返回主目录
cd "$CURRENT_DIR"
done < "$COMPONENTS_FILE"
# 生成 version.json
log_info "生成版本信息文件..."
version_json="$ARTIFACT_DIR/version.json"
# 构建依赖关系 JSON
deps_json=""
if [[ -f "$DEPENDENCIES_FILE" ]]; then
first=true
while IFS= read -r line; do
component=$(echo "$line" | cut -d':' -f1)
dep=$(echo "$line" | cut -d':' -f2)
if [[ "$first" == "true" ]]; then
deps_json="\"$component\":[\"$dep\"]"
first=false
else
deps_json="$deps_json,\"$component\":[\"$dep\"]"
fi
done < "$DEPENDENCIES_FILE"
fi
# 构建安装顺序数组
order_array=""
if [[ -f "$TEMP_DIR/install_order_files.txt" ]]; then
first=true
while IFS= read -r filename; do
if [[ "$first" == "true" ]]; then
order_array="\"$filename\""
first=false
else
order_array="$order_array,\"$filename\""
fi
done < "$TEMP_DIR/install_order_files.txt"
fi
# 构建 artifact_list JSON
artifact_json=""
if [[ -f "$ARTIFACT_LIST_FILE" ]]; then
first=true
while IFS= read -r line; do
component=$(echo "$line" | cut -d':' -f1)
version=$(echo "$line" | cut -d':' -f2)
if [[ "$first" == "true" ]]; then
artifact_json="\"$component\":\"$version\""
first=false
else
artifact_json="$artifact_json,\"$component\":\"$version\""
fi
done < "$ARTIFACT_LIST_FILE"
fi
# 构建 checksums JSON
checksums_json=""
if [[ -f "$CHECKSUMS_FILE" ]]; then
first=true
while IFS= read -r line; do
component=$(echo "$line" | cut -d':' -f1)
checksum=$(echo "$line" | cut -d':' -f2-)
if [[ "$first" == "true" ]]; then
checksums_json="\"$component\":\"$checksum\""
first=false
else
checksums_json="$checksums_json,\"$component\":\"$checksum\""
fi
done < "$CHECKSUMS_FILE"
fi
# 生成完整的 version.json
cat > "$version_json" << EOF
{
"version": "$VERSION",
"build_time": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"artifact_list": {
$artifact_json
},
"checksums": {
$checksums_json
},
"dependencies": {
$deps_json
},
"install_order": [
$order_array
]
}
EOF
log_success "版本信息文件生成完成: $version_json"
# 复制`安装`脚本到 artifact 目录
log_info "复制安装脚本..."
if [[ -f "scripts/install_artifact.sh" ]]; then
cp "scripts/install_artifact.sh" "$ARTIFACT_DIR/install.sh"
chmod +x "$ARTIFACT_DIR/install.sh"
log_success "安装脚本复制完成: $ARTIFACT_DIR/install.sh"
else
log_warning "scripts/install_artifact.sh 文件不存在"
fi
# 复制`卸载`脚本到 artifact 目录
log_info "复制卸载脚本..."
if [[ -f "scripts/uninstall_artifact.sh" ]]; then
cp "scripts/uninstall_artifact.sh" "$ARTIFACT_DIR/uninstall.sh"
chmod +x "$ARTIFACT_DIR/uninstall.sh"
log_success "卸载脚本复制完成: $ARTIFACT_DIR/uninstall.sh"
else
log_warning "scripts/uninstall_artifact.sh 文件不存在"
fi
# 复制`健康检查`脚本到 artifact 目录
log_info "复制健康检查脚本..."
if [[ -f "scripts/check_health.sh" ]]; then
cp "scripts/check_health.sh" "$ARTIFACT_DIR/check_health.sh"
chmod +x "$ARTIFACT_DIR/check_health.sh"
log_success "健康检查脚本复制完成: $ARTIFACT_DIR/check_health.sh"
else
log_warning "scripts/check_health.sh 文件不存在"
fi
# 复制`DNS 同步`脚本到 artifact 目录
log_info "复制 DNS 同步脚本..."
if [[ -f "scripts/sync_dns.sh" ]]; then
cp "scripts/sync_dns.sh" "$ARTIFACT_DIR/sync_dns.sh"
chmod +x "$ARTIFACT_DIR/sync_dns.sh"
log_success "DNS 同步脚本复制完成: $ARTIFACT_DIR/sync_dns.sh"
else
log_warning "scripts/sync_dns.sh 文件不存在"
fi
# 复制`版本校验`脚本到 artifact 目录
log_info "复制版本校验脚本..."
if [[ -f "scripts/check_version.sh" ]]; then
cp "scripts/check_version.sh" "$ARTIFACT_DIR/check_version.sh"
chmod +x "$ARTIFACT_DIR/check_version.sh"
log_success "版本校验脚本复制完成: $ARTIFACT_DIR/check_version.sh"
else
log_warning "scripts/check_version.sh 文件不存在"
fi
# 复制`自动重启`脚本到 artifact 目录
log_info "复制自动重启脚本..."
if [[ -f "scripts/restart_unhealthy.sh" ]]; then
cp "scripts/restart_unhealthy.sh" "$ARTIFACT_DIR/restart_unhealthy.sh"
chmod +x "$ARTIFACT_DIR/restart_unhealthy.sh"
log_success "自动重启脚本复制完成: $ARTIFACT_DIR/restart_unhealthy.sh"
else
log_warning "scripts/restart_unhealthy.sh 文件不存在"
fi
# 复制配置文件到 artifact 目录
log_info "复制配置文件..."
if [[ -f "config/config.env" ]]; then
cp "config/config.env" "$ARTIFACT_DIR/"
log_success "配置文件复制完成: $ARTIFACT_DIR/config.env"
else
log_warning "config 目录不存在,跳过配置文件复制"
fi
# DNS 配置文件不需要复制到版本目录,直接从 FTP 服务器根目录获取
# 复制 deps 目录到 artifact 目录
log_info "复制系统依赖包..."
if [[ -d "deps" ]]; then
cp -r "deps" "$ARTIFACT_DIR/"
log_success "系统依赖包复制完成: $ARTIFACT_DIR/deps"
# 显示deps目录内容
log_info " 依赖包列表:"
find "$ARTIFACT_DIR/deps" -name "*.tar.gz" -exec basename {} \; | while read dep_file; do
log_info " - $dep_file"
done
else
log_warning "deps 目录不存在,跳过依赖包复制"
fi
# 显示打包结果
log_success "打包完成!"
echo
echo "版本: $VERSION"
echo "输出目录: $ARTIFACT_DIR"
echo "包含组件:"
if [[ -f "$ARTIFACT_LIST_FILE" ]]; then
while IFS= read -r line; do
component=$(echo "$line" | cut -d':' -f1)
version=$(echo "$line" | cut -d':' -f2)
echo " - $component v$version"
done < "$ARTIFACT_LIST_FILE"
fi
echo
echo "文件列表:"
ls -la "$ARTIFACT_DIR"
echo
# 清理临时文件
rm -rf "$TEMP_DIR"

View File

@@ -0,0 +1,214 @@
#!/bin/bash
set -e
# 颜色定义
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 显示帮助信息
show_help() {
echo "Argus-Metric Artifact 发布脚本"
echo
echo "用法: $0 <版本号>"
echo
echo "参数:"
echo " <版本号> 要发布的版本号,对应 artifact 目录中的版本"
echo
echo "示例:"
echo " $0 1.20.0 # 发布 1.20.0 版本"
echo
}
# 检查参数
if [[ $# -ne 1 ]]; then
log_error "请提供版本号参数"
show_help
exit 1
fi
VERSION="$1"
ARTIFACT_DIR="artifact/$VERSION"
PUBLISH_DIR="/Users/sundapeng/Project/nlp/aiops/client-plugins/all-in-one/publish/"
# 检查版本目录是否存在
if [[ ! -d "$ARTIFACT_DIR" ]]; then
log_error "版本目录不存在: $ARTIFACT_DIR"
exit 1
fi
log_info "开始发布版本: $VERSION"
# 确保发布目录存在
log_info "确保发布目录存在: $PUBLISH_DIR"
mkdir -p "$PUBLISH_DIR"
# 创建临时目录用于打包
TEMP_PACKAGE_DIR="/tmp/argus-metric-package-$$"
mkdir -p "$TEMP_PACKAGE_DIR"
# 复制所有 tar.gz 文件到临时目录
log_info "准备 artifact 文件..."
tar_files=$(find "$ARTIFACT_DIR" -name "*.tar.gz" -type f)
if [[ -z "$tar_files" ]]; then
log_error "在 $ARTIFACT_DIR 中未找到 tar.gz 文件"
exit 1
fi
for file in $tar_files; do
filename=$(basename "$file")
log_info " 准备: $filename"
cp "$file" "$TEMP_PACKAGE_DIR/"
done
# 复制版本信息文件
if [[ -f "$ARTIFACT_DIR/version.json" ]]; then
log_info "复制版本信息文件..."
cp "$ARTIFACT_DIR/version.json" "$TEMP_PACKAGE_DIR/"
fi
# 复制健康检查脚本
if [[ -f "$ARTIFACT_DIR/check_health.sh" ]]; then
log_info "复制健康检查脚本..."
cp "$ARTIFACT_DIR/check_health.sh" "$TEMP_PACKAGE_DIR/"
elif [[ -f "scripts/check_health.sh" ]]; then
log_info "复制健康检查脚本 (从当前目录)..."
cp "scripts/check_health.sh" "$TEMP_PACKAGE_DIR/"
else
log_warning "未找到 check_health.sh 文件"
fi
# 复制 DNS 同步脚本
if [[ -f "$ARTIFACT_DIR/sync_dns.sh" ]]; then
log_info "复制 DNS 同步脚本..."
cp "$ARTIFACT_DIR/sync_dns.sh" "$TEMP_PACKAGE_DIR/"
elif [[ -f "scripts/sync_dns.sh" ]]; then
log_info "复制 DNS 同步脚本 (从当前目录)..."
cp "scripts/sync_dns.sh" "$TEMP_PACKAGE_DIR/"
else
log_warning "未找到 sync_dns.sh 文件"
fi
# 复制版本校验脚本
if [[ -f "$ARTIFACT_DIR/check_version.sh" ]]; then
log_info "复制版本校验脚本..."
cp "$ARTIFACT_DIR/check_version.sh" "$TEMP_PACKAGE_DIR/"
elif [[ -f "scripts/check_version.sh" ]]; then
log_info "复制版本校验脚本 (从当前目录)..."
cp "scripts/check_version.sh" "$TEMP_PACKAGE_DIR/"
else
log_warning "未找到 check_version.sh 文件"
fi
# 复制重启失败脚本
if [[ -f "$ARTIFACT_DIR/restart_unhealthy.sh" ]]; then
log_info "复制重启失败脚本..."
cp "$ARTIFACT_DIR/restart_unhealthy.sh" "$TEMP_PACKAGE_DIR/"
elif [[ -f "scripts/restart_unhealthy.sh" ]]; then
log_info "复制重启失败脚本 (从当前目录)..."
cp "scripts/restart_unhealthy.sh" "$TEMP_PACKAGE_DIR/"
else
log_warning "未找到 restart_unhealthy.sh 文件"
fi
# 复制安装脚本并重命名为 install.sh
if [[ -f "scripts/install_artifact.sh" ]]; then
log_info "复制安装脚本..."
cp "scripts/install_artifact.sh" "$TEMP_PACKAGE_DIR/install.sh"
fi
if [[ -f "scripts/uninstall_artifact.sh" ]]; then
log_info "复制卸载脚本..."
cp "scripts/uninstall_artifact.sh" "$TEMP_PACKAGE_DIR/uninstall.sh"
fi
# 复制配置文件
if [[ -f "$ARTIFACT_DIR/config.env" ]]; then
log_info "复制配置文件..."
cp "$ARTIFACT_DIR/config.env" "$TEMP_PACKAGE_DIR/"
log_success "配置文件复制完成"
else
log_warning "未找到 config.env 文件"
fi
# DNS 配置文件将在后面直接复制到发布目录根目录,不包含在 tar.gz 中
# 复制 deps 目录
if [[ -d "$ARTIFACT_DIR/deps" ]]; then
log_info "复制系统依赖包..."
cp -r "$ARTIFACT_DIR/deps" "$TEMP_PACKAGE_DIR/"
log_success "系统依赖包复制完成"
fi
# 创建tar包,使用新的命名规范
TAR_NAME="argus-metric_$(echo $VERSION | tr '.' '_').tar.gz"
log_info "创建发布包: $TAR_NAME"
cd "$TEMP_PACKAGE_DIR"
tar -czf "$PUBLISH_DIR/$TAR_NAME" *
cd - > /dev/null
# 清理临时目录
rm -rf "$TEMP_PACKAGE_DIR"
# 更新 LATEST_VERSION 文件
log_info "更新 LATEST_VERSION 文件..."
echo "$VERSION" > "$PUBLISH_DIR/LATEST_VERSION"
# 复制 DNS 配置文件到发布目录根目录(直接从 config 目录复制)
if [[ -f "config/dns.conf" ]]; then
log_info "复制 DNS 配置文件到发布目录根目录..."
cp "config/dns.conf" "$PUBLISH_DIR/"
log_success "DNS 配置文件复制完成: $PUBLISH_DIR/dns.conf"
else
log_warning "未找到 config/dns.conf 文件,跳过 DNS 配置文件复制"
fi
# 复制 setup.sh 到发布目录
if [[ -f "scripts/setup.sh" ]]; then
log_info "复制 setup.sh 到发布目录..."
cp "scripts/setup.sh" "$PUBLISH_DIR/"
fi
# 显示发布结果
log_success "版本 $VERSION 发布完成!"
echo
echo "发布目录: $PUBLISH_DIR"
echo "发布包: $PUBLISH_DIR/$TAR_NAME"
echo "包大小: $(du -h "$PUBLISH_DIR/$TAR_NAME" | cut -f1)"
echo "最新版本: $(cat "$PUBLISH_DIR/LATEST_VERSION")"
echo
echo "发布目录中的文件:"
ls -la "$PUBLISH_DIR" | while read line; do
echo " $line"
done
echo
echo "使用方法:"
echo " 1. 确保 /srv/ftp/share 目录可通过 FTP 访问"
echo " 2. 用户首先下载安装脚本:"
echo " curl -u ftpuser:admin1234 ftp://10.211.55.4/setup.sh -o setup.sh"
echo " 3. 然后执行安装 (自动获取最新版本):"
echo " sudo sh setup.sh"
echo " 4. 或者指定版本安装:"
echo " sudo sh setup.sh --version $VERSION"
echo " 5. 或者指定不同的FTP服务器:"
echo " sudo sh setup.sh --server 192.168.1.100 --user myuser --password mypass"

View File

@@ -0,0 +1,328 @@
#!/bin/bash
# 此脚本会检查各组件的健康状态,并重启不健康的组件
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_RECORD_FILE="$SCRIPT_DIR/.install_record"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
# 加载配置文件
load_config() {
local config_file="$SCRIPT_DIR/config.env"
if [[ -f "$config_file" ]]; then
log_info "加载配置文件: $config_file"
set -a
source "$config_file"
set +a
log_success "配置文件加载完成"
else
log_warning "配置文件不存在: $config_file,使用默认配置"
fi
}
# 检查单个组件健康状态
check_component_health() {
local component_name="$1"
local check_script_path="$2"
if [[ ! -f "$check_script_path" ]]; then
log_error "$component_name: 健康检查脚本不存在: $check_script_path"
return 1
fi
if [[ ! -x "$check_script_path" ]]; then
chmod +x "$check_script_path" 2>/dev/null || true
fi
# 执行健康检查,捕获退出码
if "$check_script_path" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
# 重启单个组件
restart_component() {
local component_name="$1"
local install_dir="$2"
log_warning "正在重启组件: $component_name"
# 先执行卸载脚本
local uninstall_script="$install_dir/uninstall.sh"
if [[ -f "$uninstall_script" ]]; then
log_info "$component_name: 执行卸载脚本..."
chmod +x "$uninstall_script" 2>/dev/null || true
# 使用 yes 命令自动回答所有确认提示
yes 2>/dev/null | (cd "$install_dir" && "$uninstall_script") || true
log_info "$component_name: 卸载完成"
fi
# 执行安装脚本
local install_script="$install_dir/install.sh"
if [[ ! -f "$install_script" ]]; then
log_error "$component_name: 安装脚本不存在: $install_script"
return 1
fi
chmod +x "$install_script" 2>/dev/null || true
log_info "$component_name: 执行安装脚本..."
# 使用 yes 命令自动回答所有确认提示,传递 SCRIPT_DIR 作为参数
yes 2>/dev/null | (cd "$install_dir" && "$install_script" "$SCRIPT_DIR") || true
log_info "$component_name: 安装脚本执行完成"
return 0
}
# 查找组件进程 PID
find_component_pid() {
local component_name="$1"
local component_pid=""
case "$component_name" in
"node-exporter")
component_pid=$(pgrep -f "node_exporter" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(pgrep -f "node-exporter" | head -1)
fi
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "node_exporter" | awk '{print $2}' | head -1)
fi
;;
"dcgm-exporter")
component_pid=$(pgrep -f "dcgm-exporter" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(pgrep -f "dcgm_exporter" | head -1)
fi
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "dcgm-exporter" | awk '{print $2}' | head -1)
fi
;;
"fluent-bit")
component_pid=$(pgrep -f "fluent-bit" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(pgrep -f "fluent_bit" | head -1)
fi
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "fluent-bit" | awk '{print $2}' | head -1)
fi
;;
"argus-agent")
component_pid=$(pgrep -f "argus-agent" | head -1)
if [[ -z "$component_pid" ]]; then
component_pid=$(ps aux | grep -v grep | grep "argus-agent" | awk '{print $2}' | head -1)
fi
;;
esac
echo "$component_pid"
}
# 更新安装记录文件中的 PID
update_install_record_pid() {
local component_name="$1"
local new_pid="$2"
if [[ ! -f "$INSTALL_RECORD_FILE" ]]; then
log_error "安装记录文件不存在: $INSTALL_RECORD_FILE"
return 1
fi
# 读取当前 PID
local current_pid=""
if command -v jq &> /dev/null; then
current_pid=$(jq -r --arg comp "$component_name" '.components[$comp].pid // ""' "$INSTALL_RECORD_FILE" 2>/dev/null)
fi
if [[ -z "$current_pid" ]]; then
log_warning "$component_name: 无法读取当前 PID,跳过更新"
return 1
fi
# 使用 sed 精确替换 PID,保持原有格式不变
# 只替换指定组件块中的 pid 字段
local temp_file="${INSTALL_RECORD_FILE}.tmp"
local in_component=0
local updated=0
while IFS= read -r line; do
if [[ "$line" =~ \"$component_name\":[[:space:]]*\{ ]]; then
in_component=1
echo "$line"
elif [[ $in_component -eq 1 && "$line" =~ \"pid\":[[:space:]]*\"$current_pid\" ]]; then
echo "$line" | sed "s/\"pid\": \"$current_pid\"/\"pid\": \"$new_pid\"/"
updated=1
in_component=0
else
echo "$line"
if [[ "$line" =~ ^[[:space:]]*\}[[:space:]]*$ ]]; then
in_component=0
fi
fi
done < "$INSTALL_RECORD_FILE" > "$temp_file"
# 验证替换是否成功
if [[ $updated -eq 1 ]]; then
mv "$temp_file" "$INSTALL_RECORD_FILE"
log_success "$component_name: PID 已更新为 $new_pid(原值: $current_pid)"
return 0
else
log_error "$component_name: PID 替换失败"
rm -f "$temp_file"
return 1
fi
}
# 从安装记录文件中读取组件信息
read_install_record() {
local install_record_file="$1"
if [[ ! -f "$install_record_file" ]]; then
log_error "安装记录文件不存在: $install_record_file"
return 1
fi
# 检查是否有 jq 命令来解析 JSON
if command -v jq &> /dev/null; then
# 使用 jq 解析 JSON
local components_json
if components_json=$(jq -r '.components | to_entries[] | "\(.key):\(.value.install_dir)"' "$install_record_file" 2>/dev/null); then
echo "$components_json"
return 0
else
log_error "无法解析安装记录文件 JSON 格式: $install_record_file"
return 1
fi
else
# 如果没有 jq,尝试简单的文本解析
log_warning "jq 命令不可用,尝试简单文本解析"
# 查找所有 install_dir 行
local components=()
while IFS= read -r line; do
if [[ "$line" =~ \"install_dir\":[[:space:]]*\"([^\"]+)\" ]]; then
local install_dir="${BASH_REMATCH[1]}"
# 从路径中提取组件名称
local component_name=$(basename "$install_dir")
components+=("$component_name:$install_dir")
fi
done < "$install_record_file"
if [[ ${#components[@]} -gt 0 ]]; then
printf '%s\n' "${components[@]}"
return 0
else
log_error "无法从安装记录文件中提取组件信息"
return 1
fi
fi
}
# 主函数
main() {
log_info "=========================================="
log_info " 组件自动重启检查"
log_info "=========================================="
# 检查是否是root用户
if [[ $EUID -ne 0 ]]; then
log_error "此脚本需要 root 权限运行"
exit 1
fi
# 加载配置文件
load_config
# 从安装记录文件中读取组件信息
log_info "从安装记录文件读取组件信息: $INSTALL_RECORD_FILE"
local components_info
if ! components_info=$(read_install_record "$INSTALL_RECORD_FILE"); then
log_error "无法读取安装记录文件,自动重启检查终止"
exit 1
fi
local restart_count=0
local check_count=0
# 逐个检查组件
while IFS= read -r component_info; do
if [[ -n "$component_info" ]]; then
IFS=':' read -r component_name install_dir <<< "$component_info"
check_count=$((check_count + 1))
local check_script_path="$install_dir/check_health.sh"
log_info "检查组件: $component_name"
# 检查健康状态
if check_component_health "$component_name" "$check_script_path"; then
log_success "$component_name: 运行正常"
else
log_warning "$component_name: 健康检查失败,尝试重启"
restart_count=$((restart_count + 1))
# 执行重启
restart_component "$component_name" "$install_dir"
# 等待服务启动
log_info "$component_name: 等待进程启动..."
sleep 10
# 查找新的进程 PID
local new_pid=$(find_component_pid "$component_name")
if [[ -n "$new_pid" ]]; then
log_info "$component_name: 找到新进程 PID: $new_pid"
update_install_record_pid "$component_name" "$new_pid"
else
log_warning "$component_name: 未找到新进程 PID"
fi
# 再次检查健康状态
if check_component_health "$component_name" "$check_script_path"; then
log_success "$component_name: 重启成功"
else
log_warning "$component_name: 重启后仍不健康,可能需要手动检查"
fi
fi
fi
done <<< "$components_info"
log_info "=========================================="
log_info "检查完成: 共检查 $check_count 个组件,尝试重启 $restart_count 个"
log_info "=========================================="
exit 0
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,931 @@
#!/bin/bash
set -e
# 加载配置文件(仅在解压后的目录中可用)
load_config() {
# setup.sh 脚本不需要配置文件,FTP参数通过命令行参数或环境变量提供
log_info "setup.sh 脚本使用命令行参数或环境变量获取FTP配置"
}
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
FTP_SERVER="${FTP_SERVER}"
FTP_USER="${FTP_USER}"
FTP_PASS="${FTP_PASS}"
FTP_PORT="${FTP_PORT:-21}"
BASE_URL="" # FTP基础URL (将在check_ftp_params中设置)
LATEST_VERSION_URL="" # 版本文件URL (将在check_ftp_params中设置)
TEMP_DIR="/tmp/argus-metric-install-$$"
# 安装目录配置
DEFAULT_INSTALL_DIR="/opt/argus-metric" # 默认安装目录
INSTALL_DIR="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" # 可通过环境变量覆盖
VERSIONS_DIR="$INSTALL_DIR/versions" # 版本目录
BACKUPS_DIR="$INSTALL_DIR/backups" # 备份目录
CURRENT_LINK="$INSTALL_DIR/current" # 当前版本软链接
LATEST_VERSION_FILE="$INSTALL_DIR/LATEST_VERSION" # 当前版本记录文件
# 检查必需的FTP参数
check_ftp_params() {
local missing_params=()
if [[ -z "$FTP_SERVER" ]]; then
missing_params+=("FTP_SERVER")
fi
if [[ -z "$FTP_USER" ]]; then
missing_params+=("FTP_USER")
fi
if [[ -z "$FTP_PASS" ]]; then
missing_params+=("FTP_PASS")
fi
if [[ ${#missing_params[@]} -gt 0 ]]; then
log_error "缺少必需的FTP参数: ${missing_params[*]}"
log_error "请通过以下方式之一设置FTP参数:"
log_error " 1. 命令行参数: --server <地址> --user <用户名> --password <密码>"
log_error " 2. 环境变量: FTP_SERVER=<地址> FTP_USER=<用户名> FTP_PASS=<密码>"
log_error ""
log_error "示例:"
log_error " sudo sh setup.sh --server 10.211.55.4 --user ftpuser --password admin1234"
log_error " FTP_SERVER=10.211.55.4 FTP_USER=ftpuser FTP_PASS=admin1234 sudo sh setup.sh"
exit 1
fi
# 设置BASE_URL和LATEST_VERSION_URL
BASE_URL="ftp://${FTP_SERVER}:${FTP_PORT}"
LATEST_VERSION_URL="$BASE_URL/LATEST_VERSION"
log_info "FTP配置:"
log_info " 服务器: $FTP_SERVER:$FTP_PORT"
log_info " 用户: $FTP_USER"
}
# 获取最新版本号的函数
get_latest_version() {
log_info "获取最新版本信息..." >&2
log_info "尝试从URL获取: $LATEST_VERSION_URL" >&2
# 先测试FTP连接
log_info "测试FTP连接..." >&2
if ! curl -u "${FTP_USER}:${FTP_PASS}" -sfI "$LATEST_VERSION_URL" >/dev/null 2>&1; then
log_error "无法连接到FTP服务器或文件不存在" >&2
log_error "URL: $LATEST_VERSION_URL" >&2
log_error "请检查:" >&2
log_error " 1. FTP服务器是否运行: $FTP_SERVER:$FTP_PORT" >&2
log_error " 2. 用户名密码是否正确: $FTP_USER" >&2
log_error " 3. LATEST_VERSION文件是否存在" >&2
log_error "手动测试命令: curl -u ${FTP_USER}:${FTP_PASS} ftp://${FTP_SERVER}/LATEST_VERSION" >&2
exit 1
fi
# 获取文件内容
if ! LATEST_VERSION=$(curl -u "${FTP_USER}:${FTP_PASS}" -sfL "$LATEST_VERSION_URL" 2>/dev/null | tr -d '[:space:]'); then
log_error "下载LATEST_VERSION文件失败" >&2
exit 1
fi
log_info "原始获取内容: '$LATEST_VERSION'" >&2
if [[ -z "$LATEST_VERSION" ]]; then
log_error "获取到的版本信息为空" >&2
log_error "可能的原因:" >&2
log_error " 1. LATEST_VERSION文件为空" >&2
log_error " 2. 文件内容格式不正确" >&2
log_error " 3. 网络传输问题" >&2
log_error "请检查FTP服务器上的 /srv/ftp/share/LATEST_VERSION 文件" >&2
exit 1
fi
log_info "检测到最新版本: $LATEST_VERSION" >&2
echo "$LATEST_VERSION"
}
# 解析参数
ARGUS_VERSION="" # 使用不同的变量名避免与系统VERSION冲突
ACTION="install"
FORCE_INSTALL=false
while [[ $# -gt 0 ]]; do
case $1 in
--version)
ARGUS_VERSION="$2"
shift 2
;;
--server)
FTP_SERVER="$2"
shift 2
;;
--user)
FTP_USER="$2"
shift 2
;;
--password)
FTP_PASS="$2"
shift 2
;;
--port)
FTP_PORT="$2"
shift 2
;;
--uninstall)
ACTION="uninstall"
shift
;;
--install-dir)
INSTALL_DIR="$2"
shift 2
;;
# 简化安装逻辑:不再支持回滚和备份列表功能
# --rollback)
# ACTION="rollback"
# shift
# ;;
# --backup-list)
# ACTION="backup-list"
# shift
# ;;
--status)
ACTION="status"
shift
;;
--force)
FORCE_INSTALL=true
shift
;;
--help)
echo "Argus Metric FTP在线安装脚本"
echo
echo "用法: curl -u <用户名>:<密码> ftp://<服务器>/setup.sh -o setup.sh && sh setup.sh [选项]"
echo
echo "必需参数 (必须通过命令行参数或环境变量设置):"
echo " --server SERVER FTP服务器地址 (必须)"
echo " --user USER FTP用户名 (必须)"
echo " --password PASS FTP密码 (必须)"
echo
echo "可选参数:"
echo " --version VERSION 指定版本 (默认: 自动获取最新版本)"
echo " --port PORT FTP端口 (默认: 21)"
echo " --install-dir DIR 安装目录 (默认: /opt/argus-metric)"
echo " --force 强制重新安装 (即使相同版本)"
echo " --uninstall 卸载 (自动确认)"
# echo " --rollback 回滚到上一个备份版本"
# echo " --backup-list 列出所有备份版本"
echo " --status 显示当前安装状态"
echo " --help 显示帮助"
echo
echo "环境变量:"
echo " FTP_SERVER FTP服务器地址 (必须)"
echo " FTP_USER FTP用户名 (必须)"
echo " FTP_PASS FTP密码 (必须)"
echo " FTP_PORT FTP端口 (默认: 21)"
echo
echo "示例:"
echo " # 方式1: 使用命令行参数"
echo " curl -u ftpuser:admin1234 ftp://10.211.55.4/setup.sh -o setup.sh"
echo " sudo sh setup.sh --server 10.211.55.4 --user ftpuser --password admin1234"
echo " "
echo " # 方式2: 使用环境变量"
echo " FTP_SERVER=10.211.55.4 FTP_USER=ftpuser FTP_PASS=admin1234 sudo sh setup.sh"
echo " "
echo " # 指定版本安装"
echo " sudo sh setup.sh --server 10.211.55.4 --user ftpuser --password admin1234 --version 1.30.0"
echo " "
echo " # 强制重新安装"
echo " sudo sh setup.sh --server 10.211.55.4 --user ftpuser --password admin1234 --force"
echo " "
echo " # 卸载"
echo " sudo sh setup.sh --server 10.211.55.4 --user ftpuser --password admin1234 --uninstall"
exit 0
;;
*)
log_error "未知参数: $1"
echo "使用 --help 查看帮助信息"
exit 1
;;
esac
done
# 清理函数
cleanup() {
if [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
# 创建安装目录结构
create_install_directories() {
log_info "创建安装目录结构..."
# 创建主要目录
mkdir -p "$VERSIONS_DIR"
mkdir -p "$BACKUPS_DIR"
log_success "安装目录结构创建完成: $INSTALL_DIR"
}
# 获取当前安装的版本
get_current_version() {
# 优先从LATEST_VERSION文件读取
if [[ -f "$LATEST_VERSION_FILE" ]]; then
local version_from_file=$(cat "$LATEST_VERSION_FILE" 2>/dev/null | tr -d '[:space:]')
if [[ -n "$version_from_file" ]]; then
# 确保版本号格式一致(不带v前缀)
echo "$version_from_file"
return 0
fi
fi
# 如果文件不存在或为空,从软链接读取
if [[ -L "$CURRENT_LINK" ]]; then
local current_path=$(readlink "$CURRENT_LINK")
# 从版本目录名中提取版本号(现在不带v前缀)
basename "$current_path"
else
echo ""
fi
}
# 检查是否已安装
check_installed() {
if [[ -L "$CURRENT_LINK" ]] && [[ -d "$CURRENT_LINK" ]]; then
local current_version=$(get_current_version)
if [[ -n "$current_version" ]]; then
log_info "检测到已安装版本: v$current_version"
return 0
fi
fi
return 1
}
# 更新LATEST_VERSION文件
update_latest_version_file() {
local version="$1"
log_info "更新LATEST_VERSION文件: $version"
if echo "$version" > "$LATEST_VERSION_FILE"; then
log_success "LATEST_VERSION文件已更新"
else
log_error "更新LATEST_VERSION文件失败"
return 1
fi
}
# 初始化 DNS 配置文件到系统目录
init_dns_config_to_system() {
log_info "初始化 DNS 配置文件到系统目录..."
# 系统 DNS 配置文件
local system_dns_conf="$INSTALL_DIR/dns.conf"
# 如果系统目录中还没有 dns.conf,创建一个空的占位文件
if [[ ! -f "$system_dns_conf" ]]; then
touch "$system_dns_conf"
chmod 644 "$system_dns_conf"
log_success "DNS 配置文件占位文件已创建: $system_dns_conf"
log_info "DNS 同步脚本将从 FTP 服务器下载实际的 DNS 配置"
else
log_info "DNS 配置文件已存在: $system_dns_conf"
fi
}
# 备份当前版本
backup_current_version() {
local current_version=$(get_current_version)
if [[ -z "$current_version" ]]; then
log_info "没有当前版本需要备份"
return 0
fi
# 确保备份目录存在
mkdir -p "$BACKUPS_DIR"
local backup_name="$current_version"
local backup_path="$BACKUPS_DIR/$backup_name"
log_info "备份当前版本 $current_version 到: $backup_path"
# 如果备份已存在,先删除
if [[ -d "$backup_path" ]]; then
log_info "备份版本已存在,覆盖: $backup_path"
rm -rf "$backup_path"
fi
# 复制当前版本目录(跟随软链接复制实际内容)
if cp -rL "$CURRENT_LINK" "$backup_path"; then
log_success "版本备份完成: $backup_name"
else
log_error "版本备份失败"
exit 1
fi
}
# 回滚到备份版本
rollback_to_backup() {
local backup_name="$1"
# 确保备份目录存在
mkdir -p "$BACKUPS_DIR"
local backup_path="$BACKUPS_DIR/$backup_name"
if [[ ! -d "$backup_path" ]]; then
log_error "备份不存在: $backup_path"
return 1
fi
log_info "回滚到备份版本: $backup_name"
# 停止当前服务
stop_services
# 检查是否存在对应的版本目录
local version_dir="$VERSIONS_DIR/$backup_name"
if [[ ! -d "$version_dir" ]]; then
log_info "版本目录不存在,从备份恢复版本目录: $version_dir"
# 从备份目录恢复到版本目录
mkdir -p "$VERSIONS_DIR"
cp -r "$backup_path" "$version_dir"
fi
# 恢复软链接指向版本目录
if ln -sfn "$version_dir" "$CURRENT_LINK"; then
log_success "版本回滚完成: $backup_name"
# 更新LATEST_VERSION文件
update_latest_version_file "$backup_name"
return 0
else
log_error "版本回滚失败"
return 1
fi
}
# 停止服务
stop_services() {
log_info "停止当前服务..."
# 检查服务是否正在运行
if ! check_services_running; then
log_info "服务未运行,无需停止"
return 0
fi
# 尝试使用卸载脚本停止服务
if [[ -f "$CURRENT_LINK/uninstall.sh" ]]; then
cd "$CURRENT_LINK"
chmod +x uninstall.sh
# 自动确认停止服务(避免交互式确认)
echo "y" | ./uninstall.sh >/dev/null 2>&1
local stop_exit_code=$?
if [[ $stop_exit_code -eq 0 ]]; then
log_success "服务停止完成"
else
log_warning "停止服务时出现警告,尝试手动停止"
manual_stop_services
fi
else
log_warning "未找到卸载脚本,尝试手动停止服务"
manual_stop_services
fi
}
# 手动停止服务
manual_stop_services() {
log_info "手动停止服务..."
# 停止 node_exporter
if pgrep -f "node_exporter" >/dev/null 2>&1; then
pkill -f "node_exporter" && log_info "node_exporter 已停止"
fi
# 停止 dcgm_exporter
if pgrep -f "dcgm_exporter" >/dev/null 2>&1; then
pkill -f "dcgm_exporter" && log_info "dcgm_exporter 已停止"
fi
# 等待进程完全停止
sleep 2
# 检查是否还有残留进程
if pgrep -f "node_exporter\|dcgm_exporter" >/dev/null 2>&1; then
log_warning "仍有服务进程运行,尝试强制停止"
pkill -9 -f "node_exporter\|dcgm_exporter" 2>/dev/null || true
fi
log_success "手动停止服务完成"
}
# 启动服务
start_services() {
log_info "启动服务..."
# 检查服务是否已经在运行
if check_services_running; then
log_info "服务已在运行,跳过启动"
return 0
fi
# 由于 install_artifact.sh 已经安装了所有组件并设置了健康检查定时任务
# 这里只需要简单验证服务状态即可
log_info "组件已安装完成,健康检查定时任务已设置"
log_info "服务将在健康检查时自动启动(每5分钟检查一次)"
# 等待一下让服务有时间启动
sleep 3
# 验证服务状态
if check_services_running; then
log_success "服务启动成功"
else
log_info "服务可能正在启动中,健康检查机制将自动监控"
fi
return 0
}
# 检查服务是否正在运行
check_services_running() {
# 检查常见的服务端口是否在监听
local ports=(9100 9400) # node-exporter 和 dcgm-exporter 的默认端口
for port in "${ports[@]}"; do
if netstat -tlnp 2>/dev/null | grep -q ":$port "; then
log_info "检测到服务正在端口 $port 上运行"
return 0
fi
done
# 检查相关进程
if pgrep -f "node_exporter\|dcgm_exporter" >/dev/null 2>&1; then
log_info "检测到相关服务进程正在运行"
return 0
fi
return 1
}
# 检查是否为 root 用户
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "此脚本需要 root 权限运行"
log_info "请使用: sudo sh setup.sh"
exit 1
fi
}
# 检查系统要求
check_system() {
log_info "检查系统要求..."
# 检查操作系统
if [[ ! -f /etc/os-release ]]; then
log_error "无法检测操作系统版本"
exit 1
fi
# 读取系统信息,使用子shell避免污染当前环境变量
local OS_INFO=$(source /etc/os-release && echo "$NAME $VERSION_ID")
log_info "检测到操作系统: $OS_INFO"
# 检查系统架构
arch=$(uname -m)
log_info "系统架构: $arch"
# 检查磁盘空间
available_space=$(df / | awk 'NR==2 {print $4}')
if [[ $available_space -lt 1024 ]]; then
log_warning "可用磁盘空间不足 1GB,当前可用: $(($available_space / 1024 / 1024))GB"
fi
}
# 下载并安装
install_argus_metric() {
# 如果没有指定版本,获取最新版本
if [[ -z "$ARGUS_VERSION" ]]; then
ARGUS_VERSION=$(get_latest_version)
fi
log_info "开始安装 Argus Metric v$ARGUS_VERSION..."
log_info "安装目录: $INSTALL_DIR"
# 创建安装目录结构(必须先创建,以便备份时目录存在)
create_install_directories
# 检查是否已安装
local is_upgrade=false
if check_installed; then
local current_version=$(get_current_version)
if [[ "$current_version" == "$ARGUS_VERSION" ]]; then
if [[ "$FORCE_INSTALL" == true ]]; then
log_info "检测到相同版本 v$ARGUS_VERSION,但使用了 --force 参数,将强制重新安装"
is_upgrade=true
# 简化安装逻辑:不再备份当前版本
# backup_current_version
else
log_info "版本 v$ARGUS_VERSION 已安装,无需重复安装"
log_info "如需强制重新安装,请使用 --force 参数"
return 0
fi
else
log_info "检测到版本升级: v$current_version -> v$ARGUS_VERSION"
is_upgrade=true
# 简化安装逻辑:不再备份当前版本
# backup_current_version
fi
fi
# 创建临时目录
mkdir -p "$TEMP_DIR"
cd "$TEMP_DIR"
# 下载发布包,使用新的命名规范
TAR_NAME="argus-metric_$(echo $ARGUS_VERSION | tr '.' '_').tar.gz"
log_info "下载发布包: $TAR_NAME"
log_info "从FTP服务器下载: $FTP_SERVER:$FTP_PORT, 用户: $FTP_USER"
# 构造curl命令并显示(隐藏密码)
CURL_CMD="curl -u \"${FTP_USER}:***\" -sfL \"$BASE_URL/$TAR_NAME\" -o \"$TAR_NAME\""
log_info "执行命令: $CURL_CMD"
if ! curl -u "${FTP_USER}:${FTP_PASS}" -sfL "$BASE_URL/$TAR_NAME" -o "$TAR_NAME"; then
log_error "下载发布包失败: $BASE_URL/$TAR_NAME"
log_error "完整命令: curl -u \"${FTP_USER}:${FTP_PASS}\" -sfL \"$BASE_URL/$TAR_NAME\" -o \"$TAR_NAME\""
log_error "请检查FTP服务器连接、用户名密码是否正确"
exit 1
fi
# 解压发布包到当前目录
log_info "解压发布包..."
if ! tar -xzf "$TAR_NAME"; then
log_error "解压发布包失败"
exit 1
fi
# 显示解压后的文件结构
log_info "解压后的文件结构:"
ls -la "$TEMP_DIR"
# 准备版本目录
local version_dir="$VERSIONS_DIR/$ARGUS_VERSION"
log_info "安装到版本目录: $version_dir"
# 如果升级,先停止服务
if [[ "$is_upgrade" == true ]]; then
stop_services
fi
# 创建版本目录
if [[ -d "$version_dir" ]]; then
log_info "版本目录已存在,备份后更新"
rm -rf "$version_dir"
fi
# 创建新的版本目录
mkdir -p "$version_dir"
# 移动解压的文件到版本目录
log_info "移动文件到版本目录: $TEMP_DIR/* -> $version_dir/"
# 检查源目录是否有内容
if [[ ! "$(ls -A "$TEMP_DIR" 2>/dev/null)" ]]; then
log_error "临时目录为空,无法移动文件"
exit 1
fi
# 检查目标目录是否存在
if [[ ! -d "$version_dir" ]]; then
log_error "目标版本目录不存在: $version_dir"
exit 1
fi
# 执行文件移动
if mv "$TEMP_DIR"/* "$version_dir" 2>/dev/null; then
log_success "文件移动到版本目录完成"
else
log_error "移动文件到版本目录失败"
log_error "源目录内容:"
ls -la "$TEMP_DIR" || true
log_error "目标目录状态:"
ls -la "$version_dir" || true
log_error "权限检查:"
ls -ld "$TEMP_DIR" "$version_dir" || true
exit 1
fi
# 执行安装脚本
log_info "执行安装脚本..."
cd "$version_dir"
if [[ -f "install.sh" ]]; then
chmod +x install.sh
# 传递安装根目录给安装脚本,让install_artifact.sh安装到正确的版本目录
if ./install.sh "$version_dir"; then
log_success "安装脚本执行完成"
else
log_error "安装脚本执行失败"
# 简化安装逻辑:不再自动回滚
# if [[ "$is_upgrade" == true ]]; then
# log_warning "升级失败,尝试回滚到之前版本..."
# # 确保备份目录存在
# mkdir -p "$BACKUPS_DIR"
# local latest_backup=$(ls -1t "$BACKUPS_DIR" 2>/dev/null | head -n 1)
# if [[ -n "$latest_backup" ]]; then
# rollback_to_backup "$latest_backup"
# return 1
# fi
# fi
exit 1
fi
else
log_error "未找到安装脚本 install.sh"
exit 1
fi
# 更新软链接指向新版本
log_info "更新当前版本链接..."
# 如果 current 已经存在且是目录,先删除它
if [[ -d "$CURRENT_LINK" ]] && [[ ! -L "$CURRENT_LINK" ]]; then
log_warning "发现 current 是目录而不是符号链接,正在删除..."
rm -rf "$CURRENT_LINK"
fi
if ln -sfn "$version_dir" "$CURRENT_LINK"; then
log_success "版本链接更新完成: $CURRENT_LINK -> $version_dir"
else
log_error "版本链接更新失败"
exit 1
fi
# 更新LATEST_VERSION文件
update_latest_version_file "$ARGUS_VERSION"
# 初始化 DNS 配置文件到系统目录
init_dns_config_to_system
# 启动服务
# start_services
log_success "Argus Metric v$ARGUS_VERSION 安装完成!"
# 显示安装信息
echo
log_info "安装信息:"
log_info " 版本: $ARGUS_VERSION"
log_info " 安装目录: $INSTALL_DIR"
log_info " 版本目录: $version_dir"
log_info " 当前链接: $CURRENT_LINK"
if [[ "$is_upgrade" == true ]]; then
log_info " 升级类型: 版本升级"
else
log_info " 安装类型: 全新安装"
fi
}
# 卸载
uninstall_argus_metric() {
log_info "开始卸载 Argus Metric..."
log_info "安装目录: $INSTALL_DIR"
# 检查是否已安装
if ! check_installed; then
log_info "未检测到已安装的 Argus Metric"
return 0
fi
local current_version=$(get_current_version)
log_info "检测到当前版本: v$current_version"
# 停止服务
stop_services
# 执行卸载脚本
log_info "执行卸载脚本..."
if [[ -f "$CURRENT_LINK/uninstall.sh" ]]; then
cd "$CURRENT_LINK"
chmod +x uninstall.sh
# 自动确认卸载(因为用户已经明确使用了 --uninstall 参数)
log_info "自动确认卸载操作..."
echo "y" | ./uninstall.sh
local uninstall_exit_code=$?
if [[ $uninstall_exit_code -eq 0 ]]; then
log_success "卸载脚本执行完成"
else
log_error "卸载脚本执行失败 (退出码: $uninstall_exit_code)"
exit 1
fi
else
log_warning "未找到卸载脚本,执行基本清理"
fi
# 清理安装目录
log_info "清理安装目录..."
if [[ -d "$INSTALL_DIR" ]]; then
# 询问是否完全删除安装目录
log_warning "这将删除整个安装目录: $INSTALL_DIR"
log_warning "包括所有版本、备份和配置文件"
# 在自动化环境中,直接删除
if rm -rf "$INSTALL_DIR"; then
log_success "安装目录已完全清理: $INSTALL_DIR"
else
log_error "清理安装目录失败"
exit 1
fi
else
log_info "安装目录不存在,无需清理"
fi
log_success "Argus Metric 卸载完成!"
}
# 显示状态
show_status() {
echo "=========================================="
echo " Argus Metric 安装状态"
echo "=========================================="
echo
if check_installed; then
local current_version=$(get_current_version)
log_info "当前版本: $current_version"
log_info "安装目录: $INSTALL_DIR"
log_info "当前链接: $CURRENT_LINK"
log_info "版本目录: $VERSIONS_DIR/$current_version"
log_info "版本文件: $LATEST_VERSION_FILE"
# 显示LATEST_VERSION文件内容
if [[ -f "$LATEST_VERSION_FILE" ]]; then
local file_version=$(cat "$LATEST_VERSION_FILE" 2>/dev/null | tr -d '[:space:]')
log_info "版本文件内容: $file_version"
fi
echo
log_info "目录结构:"
if [[ -d "$INSTALL_DIR" ]]; then
tree -L 2 "$INSTALL_DIR" 2>/dev/null || ls -la "$INSTALL_DIR"
fi
echo
log_info "可用版本:"
if [[ -d "$VERSIONS_DIR" ]]; then
ls -1 "$VERSIONS_DIR" 2>/dev/null | sed 's/^/ - /'
else
echo " 无"
fi
# 简化安装逻辑:不再显示备份版本信息
# echo
# log_info "备份版本:"
# if [[ -d "$BACKUPS_DIR" ]] && [[ $(ls -1 "$BACKUPS_DIR" 2>/dev/null | wc -l) -gt 0 ]]; then
# ls -1t "$BACKUPS_DIR" 2>/dev/null | sed 's/^/ - /'
# else
# echo " 无"
# fi
else
log_warning "Argus Metric 未安装"
log_info "安装目录: $INSTALL_DIR"
fi
}
# 列出备份
list_backups() {
echo "=========================================="
echo " Argus Metric 备份列表"
echo "=========================================="
echo
if [[ -d "$BACKUPS_DIR" ]] && [[ $(ls -1 "$BACKUPS_DIR" 2>/dev/null | wc -l) -gt 0 ]]; then
log_info "可用备份版本:"
ls -1t "$BACKUPS_DIR" 2>/dev/null | while read backup; do
local backup_time=$(stat -c %y "$BACKUPS_DIR/$backup" 2>/dev/null | cut -d' ' -f1-2)
echo " - $backup (创建时间: $backup_time)"
done
else
log_warning "没有可用的备份版本"
fi
}
# 回滚功能
rollback_version() {
log_info "开始回滚操作..."
if ! check_installed; then
log_error "没有检测到已安装的版本,无法回滚"
exit 1
fi
# 确保备份目录存在
mkdir -p "$BACKUPS_DIR"
# 获取最新的备份
local latest_backup=$(ls -1t "$BACKUPS_DIR" 2>/dev/null | head -n 1)
if [[ -z "$latest_backup" ]]; then
log_error "没有找到可用的备份版本"
exit 1
fi
log_info "将回滚到备份版本: $latest_backup"
if rollback_to_backup "$latest_backup"; then
log_success "回滚完成!"
# 显示当前状态
echo
show_status
else
log_error "回滚失败"
exit 1
fi
}
# 主函数
main() {
echo "=========================================="
echo " Argus Metric 在线安装脚本 v1.0"
echo "=========================================="
echo
# 加载配置文件
load_config
# 对于状态操作,不需要FTP参数和root权限
# 简化安装逻辑:不再支持备份列表操作
if [[ "$ACTION" == "status" ]]; then
show_status
return 0
fi
# if [[ "$ACTION" == "status" || "$ACTION" == "backup-list" ]]; then
# if [[ "$ACTION" == "status" ]]; then
# show_status
# elif [[ "$ACTION" == "backup-list" ]]; then
# list_backups
# fi
# return 0
# fi
check_root
# 更新目录配置变量(在设置INSTALL_DIR后)
VERSIONS_DIR="$INSTALL_DIR/versions"
BACKUPS_DIR="$INSTALL_DIR/backups"
CURRENT_LINK="$INSTALL_DIR/current"
LATEST_VERSION_FILE="$INSTALL_DIR/LATEST_VERSION"
# 简化安装逻辑:不再支持回滚操作
# if [[ "$ACTION" == "rollback" ]]; then
# rollback_version
# return 0
# fi
check_ftp_params
check_system
if [[ "$ACTION" == "uninstall" ]]; then
uninstall_argus_metric
else
install_argus_metric
fi
echo
log_info "操作完成!"
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,244 @@
#!/bin/bash
# DNS 同步脚本
# 比较 FTP 根目录的 dns.conf 和本地的 dns.conf,如果有变化则同步到 /etc/resolv.conf
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数 - 输出到 stderr 避免影响函数返回值
log_info() {
echo -e "${BLUE}[INFO]${NC} $1" >&2
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1" >&2
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOCAL_DNS_CONF="/opt/argus-metric/dns.conf"
REMOTE_DNS_CONF_URL=""
RESOLV_CONF="/etc/resolv.conf"
LOG_FILE="/opt/argus-metric/.dns_sync.log"
# 从环境变量或配置文件获取 FTP 服务器信息
get_ftp_config() {
# 优先从环境变量获取配置
log_info "获取 FTP 配置信息..."
# 如果环境变量中没有设置,则尝试从配置文件读取
if [[ -z "$FTP_SERVER" || -z "$FTP_USER" || -z "$FTP_PASSWORD" ]]; then
local config_file="$SCRIPT_DIR/config.env"
if [[ -f "$config_file" ]]; then
log_info "从配置文件读取 FTP 配置: $config_file"
source "$config_file"
fi
else
log_info "使用环境变量中的 FTP 配置"
fi
# 设置默认值(如果环境变量和配置文件都没有设置)
FTP_SERVER="${FTP_SERVER:-localhost}"
FTP_USER="${FTP_USER:-ftpuser}"
FTP_PASSWORD="${FTP_PASSWORD:-ZGClab1234!}"
# 构建远程 DNS 配置文件 URL
REMOTE_DNS_CONF_URL="ftp://${FTP_USER}:${FTP_PASSWORD}@${FTP_SERVER}/dns.conf"
log_info "FTP 配置来源: ${FTP_CONFIG_SOURCE:-环境变量/配置文件}"
}
# 下载远程 DNS 配置文件
download_remote_dns_conf() {
local temp_file="/tmp/dns.conf.remote.$$"
log_info "从 FTP 服务器下载 DNS 配置文件..."
log_info "远程地址: $REMOTE_DNS_CONF_URL"
log_info "FTP 服务器: $FTP_SERVER"
log_info "FTP 用户: $FTP_USER"
# 先测试 FTP 连接
log_info "测试 FTP 连接..."
if curl -u "${FTP_USER}:${FTP_PASSWORD}" -sfI "ftp://${FTP_SERVER}/" >/dev/null 2>&1; then
log_success "FTP 服务器连接成功"
else
log_error "无法连接到 FTP 服务器: $FTP_SERVER"
log_error "请检查:"
log_error " 1. FTP 服务器是否运行"
log_error " 2. 网络连接是否正常"
log_error " 3. 服务器地址是否正确"
return 1
fi
# 测试 dns.conf 文件是否存在
log_info "检查远程 dns.conf 文件是否存在..."
if curl -u "${FTP_USER}:${FTP_PASSWORD}" -sfI "ftp://${FTP_SERVER}/dns.conf" >/dev/null 2>&1; then
log_success "远程 dns.conf 文件存在"
else
log_error "远程 dns.conf 文件不存在或无法访问"
log_error "请检查 FTP 服务器根目录下是否有 dns.conf 文件"
return 1
fi
# 尝试下载文件
log_info "开始下载 dns.conf 文件..."
if curl -u "${FTP_USER}:${FTP_PASSWORD}" -sf "ftp://${FTP_SERVER}/dns.conf" -o "$temp_file" 2>/dev/null; then
log_success "远程 DNS 配置文件下载成功"
echo "$temp_file"
else
log_error "下载 dns.conf 文件失败"
log_error "尝试手动测试命令:"
log_error " curl -u ${FTP_USER}:${FTP_PASSWORD} ftp://${FTP_SERVER}/dns.conf"
rm -f "$temp_file"
return 1
fi
}
# 比较两个文件是否相同
compare_files() {
local file1="$1"
local file2="$2"
if [[ ! -f "$file1" || ! -f "$file2" ]]; then
return 1
fi
# 使用 diff 比较文件内容
if diff -q "$file1" "$file2" >/dev/null 2>&1; then
return 0 # 文件相同
else
return 1 # 文件不同
fi
}
# 将 DNS 配置追加到 /etc/resolv.conf
update_resolv_conf() {
local dns_conf_file="$1"
log_info "更新 /etc/resolv.conf 文件..."
# 备份原始文件
if [[ -f "$RESOLV_CONF" ]]; then
cp "$RESOLV_CONF" "${RESOLV_CONF}.backup.$(date +%Y%m%d_%H%M%S)"
log_info "已备份原始 resolv.conf 文件"
fi
# 读取 DNS 配置文件并追加到 resolv.conf
while IFS= read -r line; do
# 跳过空行和注释行
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
# 验证是否为有效的 IP 地址
if [[ "$line" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
# 检查是否已存在相同的 nameserver 行
if ! grep -q "nameserver $line" "$RESOLV_CONF" 2>/dev/null; then
echo "nameserver $line" >> "$RESOLV_CONF"
log_info "添加 DNS 服务器: $line"
else
log_info "DNS 服务器已存在,跳过: $line"
fi
else
log_warning "跳过无效的 DNS 地址: $line"
fi
done < "$dns_conf_file"
# 设置文件权限
chmod 644 "$RESOLV_CONF"
log_success "/etc/resolv.conf 文件更新完成"
}
# 记录同步日志
log_sync() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" >> "$LOG_FILE"
}
# 主函数
main() {
log_info "开始 DNS 同步检查..."
log_sync "DNS 同步检查开始"
# 确保系统目录存在
mkdir -p "/opt/argus-metric"
# 获取 FTP 配置
get_ftp_config
# 检查本地 DNS 配置文件是否存在
if [[ ! -f "$LOCAL_DNS_CONF" ]]; then
log_warning "本地 DNS 配置文件不存在: $LOCAL_DNS_CONF"
log_warning "将下载远程配置文件并更新系统 DNS 设置"
# 下载远程配置文件
if remote_file=$(download_remote_dns_conf); then
# 复制到本地
cp "$remote_file" "$LOCAL_DNS_CONF"
log_success "远程 DNS 配置文件已保存到本地"
# 更新 resolv.conf
update_resolv_conf "$LOCAL_DNS_CONF"
log_sync "首次同步完成,DNS 配置已更新"
# 清理临时文件
rm -f "$remote_file"
else
log_error "无法下载远程 DNS 配置文件,同步失败"
log_sync "同步失败:无法下载远程配置文件"
exit 1
fi
else
log_info "本地 DNS 配置文件存在: $LOCAL_DNS_CONF"
# 下载远程配置文件进行比较
if remote_file=$(download_remote_dns_conf); then
# 比较文件
if compare_files "$LOCAL_DNS_CONF" "$remote_file"; then
log_info "DNS 配置文件无变化,无需更新"
log_sync "DNS 配置文件无变化"
else
log_info "检测到 DNS 配置文件有变化,开始同步..."
log_sync "检测到 DNS 配置文件变化,开始同步"
# 更新本地配置文件
cp "$remote_file" "$LOCAL_DNS_CONF"
log_success "本地 DNS 配置文件已更新"
# 更新 resolv.conf
update_resolv_conf "$LOCAL_DNS_CONF"
log_sync "DNS 配置同步完成"
fi
# 清理临时文件
rm -f "$remote_file"
else
log_error "无法下载远程 DNS 配置文件,跳过本次同步"
log_sync "同步失败:无法下载远程配置文件"
exit 1
fi
fi
log_success "DNS 同步检查完成"
log_sync "DNS 同步检查完成"
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,274 @@
#!/bin/bash
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 配置变量
INSTALL_DIR="/opt/argus-metric"
TEMP_DIR="/tmp/argus-metric-uninstall-$$"
VERSION_FILE="version.json"
# 检查是否为 root 用户
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "此脚本需要 root 权限运行"
log_info "请使用: sudo $0"
exit 1
fi
}
# 查找版本文件
find_version_file() {
log_info "查找版本信息文件..."
# 在当前目录查找
if [[ -f "$VERSION_FILE" ]]; then
VERSION_FILE_PATH="$VERSION_FILE"
log_success "找到版本文件: $VERSION_FILE"
return 0
fi
# 在 artifact 目录查找
for version_dir in artifact/*/; do
if [[ -f "${version_dir}${VERSION_FILE}" ]]; then
VERSION_FILE_PATH="${version_dir}${VERSION_FILE}"
log_success "找到版本文件: $VERSION_FILE_PATH"
return 0
fi
done
log_error "未找到版本信息文件 $VERSION_FILE"
log_info "请确保在正确的目录下运行此脚本"
exit 1
}
# 解析版本信息
parse_version_info() {
log_info "解析版本信息..."
if [[ ! -f "$VERSION_FILE_PATH" ]]; then
log_error "版本文件不存在: $VERSION_FILE_PATH"
exit 1
fi
# 使用 jq 解析 JSON(如果可用)
if command -v jq &> /dev/null; then
VERSION=$(jq -r '.version' "$VERSION_FILE_PATH")
BUILD_TIME=$(jq -r '.build_time' "$VERSION_FILE_PATH")
# 解析 install_order(现在包含完整的文件名)
if jq -e '.install_order' "$VERSION_FILE_PATH" > /dev/null 2>&1; then
jq -r '.install_order[]' "$VERSION_FILE_PATH" > "$TEMP_DIR/install_order.txt"
else
log_error "version.json 中缺少 install_order 字段"
exit 1
fi
else
log_warning "jq 未安装,使用简单的 JSON 解析"
VERSION=$(grep '"version"' "$VERSION_FILE_PATH" | sed 's/.*"version": *"\([^"]*\)".*/\1/')
BUILD_TIME=$(grep '"build_time"' "$VERSION_FILE_PATH" | sed 's/.*"build_time": *"\([^"]*\)".*/\1/')
# 解析 install_order
grep -A 100 '"install_order"' "$VERSION_FILE_PATH" | grep -E '^\s*"[^"]+"' | while read line; do
component=$(echo "$line" | sed 's/.*"\([^"]*\)".*/\1/')
echo "$component" >> "$TEMP_DIR/install_order.txt"
done
fi
log_success "版本信息解析完成"
log_info " 版本: $VERSION"
log_info " 构建时间: $BUILD_TIME"
}
# 创建临时目录
create_temp_dirs() {
log_info "创建临时目录..."
mkdir -p "$TEMP_DIR"
log_success "临时目录创建完成: $TEMP_DIR"
}
# 卸载组件
uninstall_components() {
log_info "开始卸载组件..."
artifact_dir=$(dirname "$VERSION_FILE_PATH")
uninstall_count=0
total_count=0
if [[ -f "$TEMP_DIR/install_order.txt" ]]; then
total_count=$(wc -l < "$TEMP_DIR/install_order.txt")
fi
if [[ -f "$TEMP_DIR/install_order.txt" ]]; then
while IFS= read -r filename; do
uninstall_count=$((uninstall_count + 1))
# 从文件名中提取组件名(去掉时间戳后缀)
component=$(echo "$filename" | sed 's/-[0-9]\{8\}-[0-9]\{6\}\.tar\.gz$//')
log_info "[$uninstall_count/$total_count] 卸载 $component..."
# 直接使用完整的文件名
tar_file="$artifact_dir/$filename"
if [[ ! -f "$tar_file" ]]; then
log_error "找不到组件文件: $filename"
exit 1
fi
# 解压到临时目录
component_temp_dir="$TEMP_DIR/$component"
mkdir -p "$component_temp_dir"
if tar -xzf "$tar_file" -C "$component_temp_dir"; then
log_success " $component 解压完成"
else
log_error " $component 解压失败"
exit 1
fi
# 查找解压后的目录
extracted_dir=""
for dir in "$component_temp_dir"/*; do
if [[ -d "$dir" ]]; then
extracted_dir="$dir"
break
fi
done
if [[ -z "$extracted_dir" ]]; then
log_error " $component 解压后未找到目录"
exit 1
fi
# 执行卸载脚本
if [[ -f "$extracted_dir/uninstall.sh" ]]; then
log_info " 执行 $component 卸载脚本..."
# 所有组件都只需要一个确认
if (cd "$extracted_dir" && echo "y" | ./uninstall.sh); then
log_success " $component 卸载完成"
else
log_error " $component 卸载失败"
exit 1
fi
else
log_warning " $component 缺少 uninstall.sh 文件,跳过卸载"
fi
# 清理临时文件
rm -rf "$component_temp_dir"
done < "$TEMP_DIR/install_order.txt"
fi
log_success "所有组件卸载完成"
}
# 清理全局文件
cleanup_global_files() {
log_info "清理全局文件..."
# 清理安装目录
if [[ -d "$INSTALL_DIR" ]]; then
rm -rf "$INSTALL_DIR"
log_success "安装目录已清理: $INSTALL_DIR"
else
log_info "安装目录不存在: $INSTALL_DIR"
fi
# 清理可能的全局配置文件
local global_configs=(
"/etc/argus-metric"
"/var/log/argus-metric"
)
for config in "${global_configs[@]}"; do
if [[ -d "$config" ]]; then
rm -rf "$config"
log_success "全局配置已清理: $config"
fi
done
}
# 显示卸载信息
show_uninstall_info() {
log_success "Argus-Metrics All-in-One 卸载完成!"
echo
echo "卸载信息:"
echo " 版本: $VERSION"
echo " 构建时间: $BUILD_TIME"
echo
echo "清理内容:"
echo " - 二进制文件"
echo " - 配置文件"
echo " - 数据目录"
echo " - 进程和服务"
echo " - 全局安装目录"
echo
echo "注意:"
echo " - 系统依赖包可能仍然存在"
echo " - 如需完全清理,请手动检查并删除相关文件"
echo
}
# 清理函数
cleanup() {
if [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
fi
}
# 设置清理陷阱
trap cleanup EXIT
# 主函数
main() {
echo "=========================================="
echo " Argus-Metrics All-in-One 卸载脚本"
echo "=========================================="
echo
check_root
find_version_file
create_temp_dirs
parse_version_info
log_warning "此操作将完全卸载 Argus-Metrics All-in-One"
read -p "确认继续?(y/N): " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
log_info "取消卸载操作"
exit 0
fi
uninstall_components
cleanup_global_files
show_uninstall_info
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,350 @@
#!/bin/bash
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 显示帮助信息
show_help() {
echo "AIOps 版本管理工具"
echo
echo "用法: $0 <command> [options]"
echo
echo "命令:"
echo " bump <type> - 升级版本号 (major|minor|patch)"
echo " set <version> - 设置指定版本号"
echo " show - 显示当前版本信息"
echo " list - 列出所有版本"
echo " clean - 清理旧版本"
echo " validate - 验证版本配置"
echo
echo "示例:"
echo " $0 bump minor # 升级次版本号 1.0.0 -> 1.1.0"
echo " $0 set 2.0.0 # 设置版本为 2.0.0"
echo " $0 show # 显示当前版本"
echo " $0 list # 列出所有版本"
}
# 获取当前版本
get_current_version() {
if [[ -f "config/VERSION" ]]; then
cat config/VERSION
else
echo "0.0.0"
fi
}
# 设置版本号
set_version() {
local new_version="$1"
# 验证版本号格式
if [[ ! "$new_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log_error "无效的版本号格式: $new_version"
log_info "版本号格式应为: major.minor.patch (如: 1.2.3)"
exit 1
fi
echo "$new_version" > config/VERSION
log_success "版本号已设置为: $new_version"
}
# 升级版本号
bump_version() {
local bump_type="$1"
local current_version=$(get_current_version)
# 解析当前版本号
IFS='.' read -r major minor patch <<< "$current_version"
case "$bump_type" in
"major")
major=$((major + 1))
minor=0
patch=0
;;
"minor")
minor=$((minor + 1))
patch=0
;;
"patch")
patch=$((patch + 1))
;;
*)
log_error "无效的升级类型: $bump_type"
log_info "支持的类型: major, minor, patch"
exit 1
;;
esac
local new_version="$major.$minor.$patch"
set_version "$new_version"
log_success "版本号已从 $current_version 升级到 $new_version"
}
# 显示当前版本信息
show_version() {
local current_version=$(get_current_version)
log_info "当前版本: $current_version"
if [[ -f "config/checklist" ]]; then
echo
echo "组件清单:"
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
read -r component version dep order <<< "$line"
if [[ -n "$component" && -n "$version" ]]; then
echo " - $component v$version"
fi
done < config/checklist
fi
# 检查是否有对应的 artifact
local artifact_dir="artifact/$current_version"
if [[ -d "$artifact_dir" ]]; then
echo
echo "已构建的组件:"
for file in "$artifact_dir"/*.tar.gz; do
if [[ -f "$file" ]]; then
local filename=$(basename "$file")
local size=$(du -h "$file" | cut -f1)
echo " - $filename ($size)"
fi
done
if [[ -f "$artifact_dir/version.json" ]]; then
echo
echo "版本信息文件: $artifact_dir/version.json"
fi
else
echo
log_warning "未找到对应的构建目录: $artifact_dir"
log_info "运行 ./package.sh 进行构建"
fi
}
# 列出所有版本
list_versions() {
log_info "所有版本列表:"
echo
if [[ ! -d "artifact" ]]; then
log_warning "artifact 目录不存在"
return
fi
for version_dir in artifact/*/; do
if [[ -d "$version_dir" ]]; then
local version=$(basename "$version_dir")
local current_version=$(get_current_version)
if [[ "$version" == "$current_version" ]]; then
echo " * $version (当前版本)"
else
echo " $version"
fi
# 显示该版本的组件
local component_count=0
for file in "$version_dir"/*.tar.gz; do
if [[ -f "$file" ]]; then
component_count=$((component_count + 1))
fi
done
if [[ $component_count -gt 0 ]]; then
echo " 包含 $component_count 个组件"
fi
fi
done
}
# 清理旧版本
clean_versions() {
local current_version=$(get_current_version)
local keep_versions=5 # 保留最近5个版本
log_info "清理旧版本 (保留最近 $keep_versions 个版本)..."
if [[ ! -d "artifact" ]]; then
log_warning "artifact 目录不存在"
return
fi
# 获取所有版本目录,按修改时间排序
local versions=()
while IFS= read -r -d '' version_dir; do
versions+=("$(basename "$version_dir")")
done < <(find artifact -maxdepth 1 -type d -name "[0-9]*" -print0 | sort -z)
local total_versions=${#versions[@]}
local versions_to_remove=$((total_versions - keep_versions))
if [[ $versions_to_remove -le 0 ]]; then
log_info "无需清理,当前只有 $total_versions 个版本"
return
fi
log_info "将删除 $versions_to_remove 个旧版本..."
for ((i=0; i<versions_to_remove; i++)); do
local version="${versions[i]}"
if [[ "$version" != "$current_version" ]]; then
log_info "删除版本: $version"
rm -rf "artifact/$version"
fi
done
log_success "旧版本清理完成"
}
# 验证版本配置
validate_version() {
log_info "验证版本配置..."
local errors=0
# 检查 VERSION 文件
if [[ ! -f "config/VERSION" ]]; then
log_error "VERSION 文件不存在"
errors=$((errors + 1))
else
local version=$(get_current_version)
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log_error "VERSION 文件格式无效: $version"
errors=$((errors + 1))
else
log_success "VERSION 文件格式正确: $version"
fi
fi
# 检查 checklist 文件
if [[ ! -f "config/checklist" ]]; then
log_error "checklist 文件不存在"
errors=$((errors + 1))
else
local component_count=0
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
read -r component version dep order <<< "$line"
if [[ -n "$component" && -n "$version" ]]; then
component_count=$((component_count + 1))
# 检查组件目录是否存在
if [[ ! -d "plugins/$component" ]]; then
log_error "组件目录不存在: plugins/$component"
errors=$((errors + 1))
fi
fi
done < config/checklist
if [[ $component_count -gt 0 ]]; then
log_success "checklist 包含 $component_count 个组件"
else
log_error "checklist 中没有有效组件"
errors=$((errors + 1))
fi
fi
# 检查 package.sh 文件
if [[ ! -f "scripts/package_artifact.sh" ]]; then
log_error "package_artifact.sh 文件不存在"
errors=$((errors + 1))
else
if [[ -x "scripts/package_artifact.sh" ]]; then
log_success "package_artifact.sh 可执行"
else
log_warning "package_artifact.sh 不可执行,请运行: chmod +x scripts/package_artifact.sh"
fi
fi
# 检查 install.sh 文件
if [[ ! -f "scripts/install_artifact.sh" ]]; then
log_error "install_artifact.sh 文件不存在"
errors=$((errors + 1))
else
if [[ -x "scripts/install_artifact.sh" ]]; then
log_success "install_artifact.sh 可执行"
else
log_warning "install_artifact.sh 不可执行,请运行: chmod +x scripts/install_artifact.sh"
fi
fi
if [[ $errors -eq 0 ]]; then
log_success "版本配置验证通过"
else
log_error "发现 $errors 个配置问题"
exit 1
fi
}
# 主函数
main() {
case "${1:-}" in
"bump")
if [[ -z "${2:-}" ]]; then
log_error "请指定升级类型: major, minor, patch"
exit 1
fi
bump_version "$2"
;;
"set")
if [[ -z "${2:-}" ]]; then
log_error "请指定版本号"
exit 1
fi
set_version "$2"
;;
"show")
show_version
;;
"list")
list_versions
;;
"clean")
clean_versions
;;
"validate")
validate_version
;;
"help"|"-h"|"--help")
show_help
;;
"")
show_help
;;
*)
log_error "未知命令: $1"
echo
show_help
exit 1
;;
esac
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,75 @@
FROM ubuntu:22.04
USER root
ARG USE_INTRANET=false
# 内网 apt 源配置
RUN if [ "$USE_INTRANET" = "true" ]; then \
echo "Configuring intranet apt sources..." && \
cp /etc/apt/sources.list /etc/apt/sources.list.bak && \
echo "deb [trusted=yes] http://10.68.64.1/ubuntu2204/ jammy main" > /etc/apt/sources.list && \
echo 'Acquire::https::Verify-Peer "false";' > /etc/apt/apt.conf.d/99disable-ssl-check && \
echo 'Acquire::https::Verify-Host "false";' >> /etc/apt/apt.conf.d/99disable-ssl-check; \
fi
# 安装常用工具和FTP服务
RUN apt-get update && \
apt-get install -y supervisor net-tools inetutils-ping vim vsftpd && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 如果是部署环境替换 apt 源
RUN if [ "$USE_INTRANET" = "true" ]; then \
echo "deb [trusted=yes] https://10.92.132.52/mirrors/ubuntu2204/ jammy main" > /etc/apt/sources.list; \
fi
# supervisor 日志目录
RUN mkdir -p /var/log/supervisor
# 设置 FTP 基础路径环境变量
ENV FTP_BASE_PATH=/private/argus/ftp
# 设置域名环境变量
ENV DOMAIN=prom.ftp.argus.com
# 设置FTP用户密码环境变量
ENV FTP_PASSWORD=ZGClab1234!
# 设置用户和组ID环境变量
ARG FTP_UID=2133
ARG FTP_GID=2015
ENV FTP_UID=${FTP_UID}
ENV FTP_GID=${FTP_GID}
# 创建FTP用户和目录结构
RUN groupadd -g ${FTP_GID} ftpuser && \
useradd -u ${FTP_UID} -g ${FTP_GID} -d ${FTP_BASE_PATH}/share -s /bin/bash ftpuser && \
mkdir -p ${FTP_BASE_PATH}/share \
&& mkdir -p /private/argus/etc \
&& mkdir -p /var/log/vsftpd \
&& mkdir -p /var/run/vsftpd/empty \
&& chown -R ftpuser:ftpuser ${FTP_BASE_PATH}
# 创建vsftpd配置目录和用户列表文件
RUN mkdir -p /etc/vsftpd && \
echo "ftpuser" > /etc/vsftpd.userlist
# supervisor 配置
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# 启动脚本
COPY start-ftp-supervised.sh /usr/local/bin/start-ftp-supervised.sh
RUN chmod +x /usr/local/bin/start-ftp-supervised.sh
# vsftpd 配置文件
COPY vsftpd.conf /etc/vsftpd/vsftpd.conf
COPY dns-monitor.sh /usr/local/bin/dns-monitor.sh
RUN chmod +x /usr/local/bin/dns-monitor.sh
USER root
EXPOSE 21 20 21100-21110
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf", "-n"]

View File

@@ -0,0 +1,159 @@
# FTP 镜像配置
## 环境变量配置
### FTP_BASE_PATH
设置 FTP 数据的基础路径。
**默认值**: `/private/argus/ftp`
**用途**:
- 共享目录路径: `${FTP_BASE_PATH}/share` (用于版本发布)
- 配置文件存储路径: `/private/argus/etc/`
### DOMAIN
设置 FTP 服务的域名。
**默认值**: `ftp.metric.argus.com`
**用途**:
- 容器IP记录文件: `/private/argus/etc/${DOMAIN}`
### FTP_PASSWORD
设置 ftpuser 用户的密码。
**默认值**: `ZGClab1234!`
**用途**:
- ftpuser 用户的登录密码
## 使用示例
### 1. 使用默认配置
```bash
docker run -d \
--name ftp-server \
-p 21:21 \
-p 21100-21110:21100-21110 \
-v /host/ftp/data:/private/argus/ftp \
argus-metric-ftp:1.0.0
```
### 2. 自定义配置(运行时环境变量)
```bash
docker run -d \
--name ftp-server \
-p 21:21 \
-p 21100-21110:21100-21110 \
-e FTP_BASE_PATH=/custom/ftp/path \
-e DOMAIN=custom.ftp.domain.com \
-e FTP_PASSWORD=MySecurePassword123! \
-v /host/ftp/data:/custom/ftp/path \
argus-metric-ftp:1.0.0
```
## 目录结构
容器启动后会在 `${FTP_BASE_PATH}` 下创建以下目录结构:
```
${FTP_BASE_PATH}/
└── share/ # FTP根目录(直接挂载)
└── (用户上传的文件)
/private/argus/etc/
└── ${DOMAIN} # 容器IP记录文件
```
## vsftpd 配置说明
### 核心配置参数
根据README中的推荐配置,vsftpd.conf包含以下关键设置:
```bash
# 基本设置
local_enable=YES # 允许本地用户登录
write_enable=YES # 允许写操作(上传/删除/修改)
chroot_local_user=YES # 限制用户在自己目录中
allow_writeable_chroot=YES # 防止 chroot 错误(重要!)
# 被动模式配置
pasv_enable=YES # 启用被动模式
pasv_min_port=21100 # 被动模式最小端口
pasv_max_port=21110 # 被动模式最大端口
# 用户访问控制
userlist_enable=YES # 启用用户列表
userlist_file=/etc/vsftpd.userlist # 用户列表文件
userlist_deny=NO # 只允许列表中的用户登录
```
### 用户管理
#### 默认用户
- **用户名**: ftpuser
- **密码**: ZGClab1234! (可通过 FTP_PASSWORD 环境变量修改)
- **UID**: 2133 (与prometheus用户保持一致,可通过 FTP_UID 环境变量修改)
- **GID**: 2015 (与prometheus用户保持一致,可通过 FTP_GID 环境变量修改)
- **主目录**: ${FTP_BASE_PATH}/share (直接指向挂载目录)
- **Shell**: /bin/bash
- **用户列表**: 已添加到 `/etc/vsftpd.userlist`
#### 添加新用户
```bash
# 进入容器
docker exec -it ftp-server bash
# 添加新用户
useradd -d ${FTP_BASE_PATH}/share/newuser -s /bin/bash newuser
echo "newuser" >> /etc/vsftpd.userlist
passwd newuser
# 创建用户目录
mkdir -p ${FTP_BASE_PATH}/share/newuser
chown newuser:newuser ${FTP_BASE_PATH}/share/newuser
```
## 端口配置
- **21**: FTP 控制端口
- **20**: FTP 数据端口 (主动模式)
- **21100-21110**: 被动模式数据端口范围
### 日志文件位置
- **vsftpd 日志**: `/var/log/vsftpd/vsftpd.log`
- **supervisor 日志**: `/var/log/supervisor/`
- `supervisord.log`: supervisor 主日志
- `vsftpd.log`: vsftpd 标准输出
- `vsftpd_error.log`: vsftpd 错误输出
```bash
# 在宿主机上配置 logrotate
cat > /etc/logrotate.d/ftp-docker << EOF
/var/lib/docker/containers/*/ftp-server-*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
}
EOF
```
### FTP连接测试
```bash
# 本地测试连接
ftp localhost
curl -fsS 'ftp://ftpuser:ZGClab1234!@177.177.70.200/setup.sh' -o setup.sh
# root用户直接执行,非root用户需要使用sudo
chmod +x setup.sh
bash setup.sh --server {$域名} --user ftpuser --password 'ZGClab1234!'
```

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# DNS监控脚本 - 每10秒检查dns.conf是否有变化
# 如果有变化则执行update-dns.sh脚本
DNS_CONF="/private/argus/etc/dns.conf"
DNS_BACKUP="/tmp/dns.conf.backup"
UPDATE_SCRIPT="/private/argus/etc/update-dns.sh"
LOG_FILE="/var/log/supervisor/dns-monitor.log"
# 确保日志文件存在
touch "$LOG_FILE"
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [DNS-Monitor] $1" >> "$LOG_FILE"
}
log_message "DNS监控脚本启动"
while true; do
if [ -f "$DNS_CONF" ]; then
if [ -f "$DNS_BACKUP" ]; then
# 比较文件内容
if ! cmp -s "$DNS_CONF" "$DNS_BACKUP"; then
log_message "检测到DNS配置变化"
# 更新备份文件
cp "$DNS_CONF" "$DNS_BACKUP"
# 执行更新脚本
if [ -x "$UPDATE_SCRIPT" ]; then
log_message "执行DNS更新脚本: $UPDATE_SCRIPT"
"$UPDATE_SCRIPT" >> "$LOG_FILE" 2>&1
if [ $? -eq 0 ]; then
log_message "DNS更新脚本执行成功"
else
log_message "DNS更新脚本执行失败"
fi
else
log_message "警告: 更新脚本不存在或不可执行: $UPDATE_SCRIPT"
fi
fi
else
# 第一次检测到配置文件,执行更新脚本
if [ -x "$UPDATE_SCRIPT" ]; then
log_message "执行DNS更新脚本: $UPDATE_SCRIPT"
"$UPDATE_SCRIPT" >> "$LOG_FILE" 2>&1
if [ $? -eq 0 ]; then
log_message "DNS更新脚本执行成功"
# 第一次运行,创建备份并执行更新
cp "$DNS_CONF" "$DNS_BACKUP"
log_message "创建DNS配置备份文件"
else
log_message "DNS更新脚本执行失败"
fi
else
log_message "警告: 更新脚本不存在或不可执行: $UPDATE_SCRIPT"
fi
fi
else
log_message "警告: DNS配置文件不存在: $DNS_CONF"
fi
sleep 10
done

View File

@@ -0,0 +1,37 @@
#!/bin/bash
set -euo pipefail
echo "[INFO] Starting FTP server under supervisor..."
FTP_BASE_PATH=${FTP_BASE_PATH:-/private/argus/ftp}
DOMAIN=${DOMAIN:-ftp.metric.argus.com}
FTP_PASSWORD=${FTP_PASSWORD:-ZGClab1234!}
echo "[INFO] FTP base path: ${FTP_BASE_PATH}"
echo "[INFO] Domain: ${DOMAIN}"
echo "[INFO] Setting ftpuser password..."
# 设置ftpuser密码
echo "ftpuser:${FTP_PASSWORD}" | chpasswd
# 确保目录存在
mkdir -p ${FTP_BASE_PATH}/share
mkdir -p /private/argus/etc
mkdir -p /var/run/vsftpd/empty
# 直接使用挂载目录作为FTP根目录,无需软链接
echo "[INFO] Using ${FTP_BASE_PATH}/share as FTP root directory"
# 生成vsftpd配置文件
echo "[INFO] Generating vsftpd.conf with base path: ${FTP_BASE_PATH}"
sed "s|\${FTP_BASE_PATH}|${FTP_BASE_PATH}|g" \
/etc/vsftpd/vsftpd.conf > /tmp/vsftpd.conf
# 记录容器 IP
IP=$(ifconfig eth0 | awk '/inet /{print $2}' || hostname -i)
echo "current IP: ${IP}"
echo "${IP}" > /private/argus/etc/${DOMAIN}
# 启动vsftpd
echo "[INFO] Starting vsftpd..."
exec /usr/sbin/vsftpd /tmp/vsftpd.conf

View File

@@ -0,0 +1,39 @@
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
user=root
[program:vsftpd]
command=/usr/local/bin/start-ftp-supervised.sh
user=root
stdout_logfile=/var/log/supervisor/vsftpd.log
stderr_logfile=/var/log/supervisor/vsftpd_error.log
autorestart=true
startretries=3
startsecs=10
stopwaitsecs=30
killasgroup=true
stopasgroup=true
[program:dns-monitor]
command=/usr/local/bin/dns-monitor.sh
user=root
stdout_logfile=/var/log/supervisor/dns-monitor.log
stderr_logfile=/var/log/supervisor/dns-monitor_error.log
autorestart=true
startretries=3
startsecs=5
stopwaitsecs=10
killasgroup=true
stopasgroup=true
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

View File

@@ -0,0 +1,111 @@
# vsftpd 配置
配置 vsftpd FTP 服务器。
# 安装deps下 vsftpd 的离线安装包
sudo dpkg -i vsftpd_3.0.5-0ubuntu1.1_amd64.deb
# 有依赖问题,修复依赖
sudo apt-get install -f
## 启动服务
sudo service vsftpd start
# 重启服务
sudo service vsftpd restart
# 查看状态
sudo service vsftpd status
## 备份配置文件
先备份默认配置,出问题能恢复:
```bash
sudo cp /etc/vsftpd.conf /etc/vsftpd.conf.bak
```
## 修改配置文件
编辑配置:
```bash
sudo vim /etc/vsftpd.conf
```
### 基本配置参数
```bash
# 允许本地用户登录
local_enable=YES
# 允许写操作(上传/删除/修改)
write_enable=YES
# 限制用户在自己目录中,不能访问整个系统
chroot_local_user=YES
# 防止 chroot 错误(重要!)
allow_writeable_chroot=YES
# 被动模式配置
pasv_enable=YES
pasv_min_port=30000
pasv_max_port=31000
```
## 创建 FTP 目录和用户
### 创建共享目录
```bash
sudo mkdir -p /srv/ftp/share
sudo chmod 755 /srv/ftp/share
```
### 创建专用用户
```bash
sudo adduser ftpuser
# 修改用户主目录
sudo usermod -d /srv/ftp/share ftpuser
```
## 重启服务
```bash
sudo service vsftpd restart
```
## 防火墙配置
### 开放基本端口
```bash
sudo ufw allow 21/tcp
```
### 开放被动模式端口
```bash
sudo ufw allow 30000:31000/tcp
```
## 测试连接
```bash
# 本地测试
ftp localhost
# 远程测试
ftp 你的服务器IP
```
用户名:ftpuser
密码:设置的密码

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# vsftpd 离线安装脚本
# 使用方法:./vsftpd-offline-install.sh
set -e
echo "开始 vsftpd 离线安装..."
# 检查是否为 root 用户
if [ "$EUID" -ne 0 ]; then
echo "请使用 root 权限运行此脚本"
exit 1
fi
# 定义离线包目录
OFFLINE_DIR="./vsftpd-offline"
DEB_DIR="$OFFLINE_DIR/debs"
# 检查离线包是否存在
if [ ! -d "$OFFLINE_DIR" ]; then
echo "错误:找不到离线包目录 $OFFLINE_DIR"
echo "请先准备离线包,方法:"
echo "1. 在有网络的机器上运行:"
echo " mkdir -p $DEB_DIR"
echo " cd $DEB_DIR"
echo " apt download vsftpd"
echo " apt download \$(apt-cache depends vsftpd | grep Depends | cut -d: -f2 | tr -d ' ')"
echo "2. 将整个 $OFFLINE_DIR 目录拷贝到目标机器"
exit 1
fi
# 安装 deb 包
echo "安装 vsftpd 及依赖包..."
cd "$DEB_DIR"
dpkg -i *.deb || apt-get install -f -y
# 检查安装状态
if systemctl is-active --quiet vsftpd; then
echo "vsftpd 安装成功并已启动"
else
echo "启动 vsftpd 服务..."
systemctl start vsftpd
systemctl enable vsftpd
fi
echo "vsftpd 离线安装完成!"
echo "配置文件位置: /etc/vsftpd.conf"
echo "服务状态: $(systemctl is-active vsftpd)"

View File

@@ -0,0 +1,56 @@
# vsftpd 配置文件
# 基本设置
listen=YES
listen_ipv6=NO
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
# 安全设置
chroot_local_user=YES
allow_writeable_chroot=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
ssl_enable=NO
# 用户设置
userlist_enable=YES
userlist_file=/etc/vsftpd.userlist
userlist_deny=NO
# 目录设置
local_root=${FTP_BASE_PATH}/share
# 被动模式设置
pasv_enable=YES
pasv_min_port=21100
pasv_max_port=21110
pasv_address=0.0.0.0
# 日志设置
xferlog_file=/var/log/vsftpd/vsftpd.log
log_ftp_protocol=YES
# 其他设置
hide_ids=YES
tcp_wrappers=YES
# 文件上传设置
file_open_mode=0666
local_umask=022
# 超时设置
idle_session_timeout=300
data_connection_timeout=300
# 限制设置
max_clients=50
max_per_ip=5

Some files were not shown because too many files have changed in this diff Show More