This commit is contained in:
anlicheng 2026-06-01 15:01:57 +08:00
parent 16e63a5c74
commit 719b539b3f
4 changed files with 105 additions and 37 deletions

View File

@ -152,6 +152,15 @@ strip_archive_suffix() {
esac esac
} }
archive_name_from_url() {
local download_url="$1"
local clean_url="$download_url"
clean_url="${clean_url%%\?*}"
clean_url="${clean_url%%#*}"
basename "$clean_url"
}
parse_args() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -394,21 +403,17 @@ install_program() {
local stop_command="$4" local stop_command="$4"
local service_user="$5" local service_user="$5"
local service_group="$6" local service_group="$6"
local clean_url="$download_url"
local archive_name local archive_name
validate_install_item "$service_name" "$download_url" "$start_command" validate_install_item "$service_name" "$download_url" "$start_command"
clean_url="${clean_url%%\?*}" archive_name="$(archive_name_from_url "$download_url")"
clean_url="${clean_url%%#*}"
archive_name="$(basename "$clean_url")"
[[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url" [[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url"
local app_dir_name strip_archive_suffix "$archive_name" >/dev/null
app_dir_name="$(strip_archive_suffix "$archive_name")"
local archive_path="${WORK_DIR}/${archive_name}" local archive_path="${WORK_DIR}/${archive_name}"
local install_dir="${WORK_DIR}/${app_dir_name}" local install_dir="${WORK_DIR}/${service_name}"
local service_file="/etc/systemd/system/${service_name}.service" local service_file="/etc/systemd/system/${service_name}.service"
local temp_archive local temp_archive
temp_archive="$(mktemp)" temp_archive="$(mktemp)"
@ -429,6 +434,9 @@ install_program() {
log "Extracting archive to $install_dir" log "Extracting archive to $install_dir"
run_as_root tar -zxvf "$archive_path" -C "$install_dir" run_as_root tar -zxvf "$archive_path" -C "$install_dir"
log "Removing downloaded archive: $archive_path"
run_as_root rm -f "$archive_path"
log "Setting owner for install directory: ${service_user}:${service_group}" log "Setting owner for install directory: ${service_user}:${service_group}"
run_as_root chown -R "${service_user}:${service_group}" "$install_dir" run_as_root chown -R "${service_user}:${service_group}" "$install_dir"

View File

@ -158,6 +158,15 @@ strip_archive_suffix() {
esac esac
} }
archive_name_from_url() {
local download_url="$1"
local clean_url="$download_url"
clean_url="${clean_url%%\?*}"
clean_url="${clean_url%%#*}"
basename "$clean_url"
}
parse_args() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -441,6 +450,18 @@ create_parent_dependency_dirs_from_env() {
done < <(service_parent_dir_env_keys "$service_name") done < <(service_parent_dir_env_keys "$service_name")
} }
link_iot_ctrl() {
local install_dir="$1"
local iot_ctrl_path="${install_dir}/bin/iot_ctrl"
local link_path="/usr/local/bin/iot_ctrl"
[[ -x "$iot_ctrl_path" ]] || fail "Missing executable iot control tool: $iot_ctrl_path. Build it before packaging the release"
log "Linking iot control tool: $link_path -> $iot_ctrl_path"
run_as_root install -d -m 0755 "$(dirname "$link_path")"
run_as_root ln -sfn "$iot_ctrl_path" "$link_path"
}
install_program() { install_program() {
local service_name="$1" local service_name="$1"
local download_url="$2" local download_url="$2"
@ -448,21 +469,17 @@ install_program() {
local stop_command="$4" local stop_command="$4"
local service_user="$5" local service_user="$5"
local service_group="$6" local service_group="$6"
local clean_url="$download_url"
local archive_name local archive_name
validate_install_item "$service_name" "$download_url" "$start_command" validate_install_item "$service_name" "$download_url" "$start_command"
clean_url="${clean_url%%\?*}" archive_name="$(archive_name_from_url "$download_url")"
clean_url="${clean_url%%#*}"
archive_name="$(basename "$clean_url")"
[[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url" [[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url"
local app_dir_name strip_archive_suffix "$archive_name" >/dev/null
app_dir_name="$(strip_archive_suffix "$archive_name")"
local archive_path="${WORK_DIR}/${archive_name}" local archive_path="${WORK_DIR}/${archive_name}"
local install_dir="${WORK_DIR}/${app_dir_name}" local install_dir="${WORK_DIR}/${service_name}"
local service_file="/etc/systemd/system/${service_name}.service" local service_file="/etc/systemd/system/${service_name}.service"
local temp_archive local temp_archive
temp_archive="$(mktemp)" temp_archive="$(mktemp)"
@ -483,11 +500,15 @@ install_program() {
log "Extracting archive to $install_dir" log "Extracting archive to $install_dir"
run_as_root tar -zxvf "$archive_path" -C "$install_dir" run_as_root tar -zxvf "$archive_path" -C "$install_dir"
log "Removing downloaded archive: $archive_path"
run_as_root rm -f "$archive_path"
log "Setting owner for install directory: ${service_user}:${service_group}" log "Setting owner for install directory: ${service_user}:${service_group}"
run_as_root chown -R "${service_user}:${service_group}" "$install_dir" run_as_root chown -R "${service_user}:${service_group}" "$install_dir"
create_dependency_dirs_from_env "$service_name" "$install_dir" "$service_user" "$service_group" create_dependency_dirs_from_env "$service_name" "$install_dir" "$service_user" "$service_group"
create_parent_dependency_dirs_from_env "$service_name" "$install_dir" "$service_user" "$service_group" create_parent_dependency_dirs_from_env "$service_name" "$install_dir" "$service_user" "$service_group"
link_iot_ctrl "$install_dir"
write_service_file "$service_file" "$service_name" "$service_user" "$service_group" "$install_dir" "$start_command" "$stop_command" write_service_file "$service_file" "$service_name" "$service_user" "$service_group" "$install_dir" "$start_command" "$stop_command"
INSTALLED_SERVICES+=("$service_name") INSTALLED_SERVICES+=("$service_name")
@ -549,6 +570,7 @@ main() {
require_command wget require_command wget
require_root_runner require_root_runner
require_command dirname require_command dirname
require_command ln
require_command tar require_command tar
require_command systemctl require_command systemctl

View File

@ -13,11 +13,13 @@
- 下载服务发布包。 - 下载服务发布包。
- 解压到 `WORK_DIR` - 解压到 `WORK_DIR`
- 删除下载到 `WORK_DIR` 的压缩包。
- 创建运行所需目录。 - 创建运行所需目录。
- 生成 `/etc/systemd/system/<service>.service` - 生成 `/etc/systemd/system/<service>.service`
- 将服务环境变量写入对应 `.service` 文件。 - 将服务环境变量写入对应 `.service` 文件。
- 执行 `systemctl daemon-reload` - 执行 `systemctl daemon-reload`
- 设置服务开机启动。 - 设置服务开机启动。
- `install_iot.sh` 会将 `{install_dir}/bin/iot_ctrl` 软链接到 `/usr/local/bin/iot_ctrl`
## 入口规则 ## 入口规则
@ -72,7 +74,7 @@ INSTALL_<APP>=(
命令中可使用占位符: 命令中可使用占位符:
- `{install_dir}`: 解压后的服务安装目录。 - `{install_dir}`: 解压后的服务安装目录,当前为 `{work_dir}/{service_name}`,例如 `/opt/app/iot``/opt/app/efka`
- `{work_dir}`: 脚本工作目录。 - `{work_dir}`: 脚本工作目录。
- `{service_name}`: 当前服务名。 - `{service_name}`: 当前服务名。
@ -123,7 +125,7 @@ Environment="KEY=value"
1. 解析命令行参数。 1. 解析命令行参数。
2. 校验 `WORK_DIR` 和服务配置。 2. 校验 `WORK_DIR` 和服务配置。
3. 检查依赖命令:`wget``dirname``tar`、`systemctl` 3. 检查依赖命令:`wget``dirname``ln`、`tar`、`systemctl`
4. 创建 `WORK_DIR` 4. 创建 `WORK_DIR`
5. 安装当前服务。 5. 安装当前服务。
6. 重新加载 systemd。 6. 重新加载 systemd。
@ -132,15 +134,51 @@ Environment="KEY=value"
单个服务安装流程: 单个服务安装流程:
1. 校验服务名、下载地址、启动命令。 1. 校验服务名、下载地址、启动命令。
2. 根据下载地址推导压缩包名和安装目录 2. 根据服务名推导安装目录,默认是 `/opt/app/<service_name>`
3. 下载压缩包到临时文件。 3. 下载压缩包到临时文件。
4. 保存压缩包到 `WORK_DIR` 4. 保存压缩包到 `WORK_DIR`
5. 创建安装目录。 5. 创建安装目录。
6. 解压压缩包。 6. 解压压缩包。
7. 设置安装目录 owner。 7. 删除保存到 `WORK_DIR` 的压缩包。
8. 根据 `*_DIR_ENV_KEYS` 创建数据目录,并根据 `*_PARENT_DIR_ENV_KEYS` 创建文件路径父目录。 8. 设置安装目录 owner。
9. 生成 systemd service 文件。 9. 根据 `*_DIR_ENV_KEYS` 创建数据目录,并根据 `*_PARENT_DIR_ENV_KEYS` 创建文件路径父目录。
10. 记录服务名,后续统一 enable。 10. iot 安装时创建 `/usr/local/bin/iot_ctrl` 软链接。
11. 生成 systemd service 文件。
12. 记录服务名,后续统一 enable。
## iot_ctrl 构建和打包原则
`iot_ctrl` 位于 `iot/go_extend`,应在发布包构建阶段编译,不在 `install_iot.sh` 中编译。
目标机器不应依赖 Go 编译环境。发布包需要提前包含:
```text
iot/
bin/
iot
iot_ctrl
```
安装后脚本会检查 `{install_dir}/bin/iot_ctrl` 是否存在且可执行。如果不存在,安装会失败并提示需要在打包前构建该工具。
安装成功后会创建软链接:
```text
/usr/local/bin/iot_ctrl -> /opt/app/iot/bin/iot_ctrl
```
构建示例:
```bash
cd iot/go_extend
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 make build
```
如果目标机器是 arm64
```bash
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 make build
```
## systemd service 生成规则 ## systemd service 生成规则
@ -233,6 +271,8 @@ SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn "$SERVICE_USER")}"
如果该变量是目录,需要加入对应 `*_DIR_ENV_KEYS`。如果该变量是文件路径,需要加入对应 `*_PARENT_DIR_ENV_KEYS` 如果该变量是目录,需要加入对应 `*_DIR_ENV_KEYS`。如果该变量是文件路径,需要加入对应 `*_PARENT_DIR_ENV_KEYS`
`{install_dir}` 不从压缩包文件名推导,不带版本号,固定使用服务名作为目录名。
## 修改和扩展原则 ## 修改和扩展原则
### 修改环境变量 ### 修改环境变量
@ -309,8 +349,8 @@ bash -n shell/install_efka.sh
建议额外验证 service 渲染结果: 建议额外验证 service 渲染结果:
```bash ```bash
bash -c "$(sed '$d' shell/install_iot.sh); render_service_file iot app app /opt/app/iot-0.1.0 '/opt/app/iot-0.1.0/bin/iot foreground' '/opt/app/iot-0.1.0/bin/iot stop'" bash -c "$(sed '$d' shell/install_iot.sh); render_service_file iot app app /opt/app/iot '/opt/app/iot/bin/iot foreground' '/opt/app/iot/bin/iot stop'"
bash -c "$(sed '$d' shell/install_efka.sh); render_service_file efka app app /opt/app/efka-0.1.0 '/opt/app/efka-0.1.0/bin/efka foreground' '/opt/app/efka-0.1.0/bin/efka stop'" bash -c "$(sed '$d' shell/install_efka.sh); render_service_file efka app app /opt/app/efka '/opt/app/efka/bin/efka foreground' '/opt/app/efka/bin/efka stop'"
``` ```
如果目标机器有 `systemd-analyze`,可进一步验证生成后的 unit 文件: 如果目标机器有 `systemd-analyze`,可进一步验证生成后的 unit 文件:
@ -325,4 +365,5 @@ systemd-analyze verify /etc/systemd/system/efka.service
- 环境变量中如包含空格或特殊字符,需要确认 systemd `Environment=` 语法是否仍然正确。 - 环境变量中如包含空格或特殊字符,需要确认 systemd `Environment=` 语法是否仍然正确。
- token 等敏感信息会写入 systemd service 文件,需控制 `/etc/systemd/system/*.service` 的读取权限和服务器访问权限。 - token 等敏感信息会写入 systemd service 文件,需控制 `/etc/systemd/system/*.service` 的读取权限和服务器访问权限。
- 数据目录和文件路径型变量必须使用绝对路径。 - 数据目录和文件路径型变量必须使用绝对路径。
- iot 发布包必须包含可执行的 `bin/iot_ctrl`,否则不会创建 `/usr/local/bin/iot_ctrl` 链接。
- 现有卸载脚本如果需要同步目录清理规则,应按 `IOT_DIR_ENV_KEYS` / `EFKA_DIR_ENV_KEYS` 的思路同步调整。 - 现有卸载脚本如果需要同步目录清理规则,应按 `IOT_DIR_ENV_KEYS` / `EFKA_DIR_ENV_KEYS` 的思路同步调整。

View File

@ -27,7 +27,8 @@ Default behavior:
- Disable services from boot. - Disable services from boot.
- Remove /etc/systemd/system/<service>.service and service backups. - Remove /etc/systemd/system/<service>.service and service backups.
- Run systemctl daemon-reload and reset-failed. - Run systemctl daemon-reload and reset-failed.
- Remove downloaded archives and extracted program directories under WORK_DIR. - Remove downloaded archives and service-name program directories under WORK_DIR.
- Remove /usr/local/bin/iot_ctrl.
- Keep dependency/data directories unless --purge-data is passed. - Keep dependency/data directories unless --purge-data is passed.
EOF EOF
} }
@ -100,16 +101,6 @@ validate_args() {
[[ "$item_count" -gt 0 ]] || fail "No programs configured. Fill INSTALL_ITEMS inside this script" [[ "$item_count" -gt 0 ]] || fail "No programs configured. Fill INSTALL_ITEMS inside this script"
} }
strip_archive_suffix() {
local filename="$1"
case "$filename" in
*.tar.gz) printf '%s\n' "${filename%.tar.gz}" ;;
*.tgz) printf '%s\n' "${filename%.tgz}" ;;
*) fail "Unsupported archive suffix: $filename. Expected .tar.gz or .tgz" ;;
esac
}
archive_name_from_url() { archive_name_from_url() {
local download_url="$1" local download_url="$1"
local clean_url="$download_url" local clean_url="$download_url"
@ -191,7 +182,6 @@ remove_program_files() {
local service_user local service_user
local service_group local service_group
local archive_name local archive_name
local app_dir_name
local archive_path local archive_path
local install_dir local install_dir
@ -205,9 +195,8 @@ remove_program_files() {
archive_name="$(archive_name_from_url "$download_url")" archive_name="$(archive_name_from_url "$download_url")"
[[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url" [[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url"
app_dir_name="$(strip_archive_suffix "$archive_name")"
archive_path="${WORK_DIR}/${archive_name}" archive_path="${WORK_DIR}/${archive_name}"
install_dir="${WORK_DIR}/${app_dir_name}" install_dir="${WORK_DIR}/${service_name}"
log "Removing downloaded archive: $archive_path" log "Removing downloaded archive: $archive_path"
run_as_root rm -f "$archive_path" run_as_root rm -f "$archive_path"
@ -223,6 +212,13 @@ remove_program_files() {
set -u set -u
} }
remove_global_links() {
local link_path="/usr/local/bin/iot_ctrl"
log "Removing global command link: $link_path"
run_as_root rm -f "$link_path"
}
remove_dependency_dirs() { remove_dependency_dirs() {
local dependency_dirs="$1" local dependency_dirs="$1"
local install_dir="$2" local install_dir="$2"
@ -263,6 +259,7 @@ main() {
stop_and_disable_services stop_and_disable_services
remove_service_files remove_service_files
reload_systemd reload_systemd
remove_global_links
remove_program_files remove_program_files
log "Uninstall completed" log "Uninstall completed"