Files
CLICD/install.sh
T

1616 lines
58 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -eu
REPO="${CLICD_REPO:-MengMengCode/CLICD}"
CLICD_INSTALL_VERSION="${CLICD_VERSION:-latest}"
ASSET="clicd-linux-amd64.tar.gz"
ACTION="${1:-install}"
ACTION_CONFIRM="${2:-}"
ISSUE_URL="https://github.com/${REPO}/issues"
LOG_FILE="${CLICD_LOG_FILE:-/var/log/clicd-install.log}"
INSTALL_DOWNLOAD_MARKER="${CLICD_INSTALL_DOWNLOAD_MARKER:-/tmp/clicd-install-dir.$$}"
LIBVIRT_DEFAULT_MARKER="/var/lib/clicd/kvm/default-network.created"
normalize_lang() {
lang="$1"
case "$(printf '%s' "$lang" | tr 'A-Z' 'a-z')" in
en|en_*|en-*) echo en ;;
zh|zh_*|zh-*) echo zh ;;
*) echo "" ;;
esac
}
detect_lang() {
normalized="$(normalize_lang "${CLICD_LANG:-}")"
if [ -n "$normalized" ]; then
echo "$normalized"
return
fi
normalized="$(normalize_lang "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}")"
if [ -n "$normalized" ]; then
echo "$normalized"
return
fi
echo zh
}
choose_language() {
normalized="$(normalize_lang "${CLICD_LANG:-}")"
if [ -n "$normalized" ]; then
echo "$normalized"
return
fi
case "$ACTION" in
-h|--help|help)
detect_lang
return
;;
esac
if [ -t 0 ]; then
{
echo "====================================="
echo " 请选择语言 / Select language"
echo "====================================="
echo " 1) 简体中文"
echo " 2) English"
printf " 请输入 1/2 [1]: "
} >&2
IFS= read -r answer || answer=""
case "$answer" in
2) echo en ;;
*) echo zh ;;
esac
return
fi
if [ -r /dev/tty ] && [ -w /dev/tty ] && { printf '' > /dev/tty; } 2>/dev/null; then
{
echo "====================================="
echo " 请选择语言 / Select language"
echo "====================================="
echo " 1) 简体中文"
echo " 2) English"
printf " 请输入 1/2 [1]: "
} > /dev/tty
IFS= read -r answer < /dev/tty || answer=""
case "$answer" in
2) echo en ;;
*) echo zh ;;
esac
return
fi
detect_lang
}
CLICD_LANG_DETECTED="$(choose_language)"
export CLICD_LANG="$CLICD_LANG_DETECTED"
tr_msg() {
msg="$*"
[ "$CLICD_LANG_DETECTED" = "en" ] || { printf '%s' "$msg"; return; }
msg="$(printf '%s' "$msg" | sed \
-e 's/中文安装\/卸载脚本/Installer\/Uninstaller/g' \
-e 's/警告/Warning/g' \
-e 's/错误/Error/g' \
-e 's/安装\/卸载未完成。请查看日志:/Install\/uninstall did not complete. Check log: /g' \
-e 's/如果你确认这是程序问题,请提交 issue/If this looks like a CLICD bug, please open an issue: /g' \
-e 's/开始:/Starting: /g' \
-e 's/完成:/Completed: /g' \
-e 's/步骤失败:/Step failed: /g' \
-e 's/退出码:/exit code: /g' \
-e 's/最近 80 行日志:/Last 80 log lines: /g' \
-e 's/请将上述日志和系统信息提交到:/Please submit the log above and system info to: /g' \
-e 's/系统检测:/System check: /g' \
-e 's/当前安装包仅支持/This installer only supports/g' \
-e 's/当前架构:/current architecture: /g' \
-e 's/未检测到 systemd 或 OpenRC,无法安装服务。/systemd or OpenRC was not detected; cannot install service./g' \
-e 's/暂不支持当前 Linux 发行版:/Unsupported Linux distribution: /g' \
-e 's/请提交 issue 并附上/Please open an issue with/g' \
-e 's/发行版/Distribution/g' \
-e 's/不在主要支持列表,将按检测到的软件包管理器尝试安装。/is not in the primary support list; trying the detected package manager./g' \
-e 's/存储检测:/Storage check: /g' \
-e 's/根文件系统=/root filesystem=/g' \
-e 's/可用空间=/available space=/g' \
-e 's/根分区可用空间低于 5GB,下载镜像或创建 KVM\/LXC 时可能失败。/Root partition has less than 5GB available; image downloads or KVM\/LXC creation may fail./g' \
-e 's/请使用 root 权限运行:/Run as root: /g' \
-e 's/或执行:/Or run: /g' \
-e 's/卸载:/Uninstall: /g' \
-e 's/问题反馈:/Issues: /g' \
-e 's/日志文件:/Log file: /g' \
-e 's/仓库地址:/Repository: /g' \
-e 's/未知操作:/Unknown action: /g' \
-e 's/卸载会停止并删除 CLICD 服务、配置数据库、CLICD 创建的 LXC\/KVM 实例和缓存数据。/Uninstall will stop and remove the CLICD service, configuration database, CLICD-created LXC\/KVM instances, and cached data./g' \
-e 's/为避免误删生产数据,脚本只会删除名称形如 ct-数字 的 LXC 容器、clicd-img-dl-\* 下载临时容器和 vm-数字 的 KVM 域。/To avoid deleting production data, the script only removes LXC containers named ct-NUMBER, temporary clicd-img-dl-* download containers, and KVM domains named vm-NUMBER./g' \
-e 's/如需确认卸载,请输入:YES/Type YES to confirm uninstall:/g' \
-e 's/已取消卸载。如需非交互卸载,请设置 CLICD_UNINSTALL_CONFIRM=1。/Uninstall cancelled. For non-interactive uninstall, set CLICD_UNINSTALL_CONFIRM=1./g' \
-e 's/正在卸载 CLICD.../Uninstalling CLICD.../g' \
-e 's/正在删除 CLICD 创建的 LXC 容器(\/var\/lib\/lxc\/ct-数字).../Removing CLICD-created LXC containers (\/var\/lib\/lxc\/ct-NUMBER).../g' \
-e 's/保留 \/root\/clicd-backups,避免误删部署\/回滚备份。确认不需要后可手动删除。/Keeping \/root\/clicd-backups to avoid deleting deployment\/rollback backups. Remove it manually if no longer needed./g' \
-e 's/CLICD 卸载完成/CLICD uninstall complete/g' \
-e 's/已删除服务、二进制、SQLite\/配置数据、CLICD LXC\/KVM 实例、/Removed service, binary, SQLite\/config data, CLICD LXC\/KVM instances,/g' \
-e 's/CLICD 镜像缓存、防火墙规则、主机钩子、配额记录和临时文件。/CLICD image cache, firewall rules, host hooks, quota records, and temporary files./g' \
-e 's/已保留 \/root\/clicd-backups 和非 CLICD 的 LXC 全局缓存,避免误删生产备份\/共享镜像。/Kept \/root\/clicd-backups and non-CLICD global LXC cache to avoid deleting production backups\/shared images./g' \
-e 's/日志:/Log: /g' \
-e 's/兼容性检查/Compatibility check/g' \
-e 's/存储环境检查/Storage environment check/g' \
-e 's/安装系统依赖/Install system dependencies/g' \
-e 's/配置内核网络参数/Configure kernel networking/g' \
-e 's/配置运行时服务/Configure runtime services/g' \
-e 's/配置 libvirt default NAT 网络/Configure libvirt default NAT network/g' \
-e 's/配置 UID\/GID 映射/Configure UID\/GID mapping/g' \
-e 's/配置 LXC 存储权限/Configure LXC storage permissions/g' \
-e 's/检查 project quota/Check project quota/g' \
-e 's/下载发行版包/Download release package/g' \
-e 's/安装 CLICD 二进制/Install CLICD binary/g' \
-e 's/安装并启动 CLICD 服务/Install and start CLICD service/g' \
-e 's/已写入面板语言:/Panel language saved: /g' \
-e 's/写入面板语言/Save panel language/g' \
-e 's/面板语言写入失败,请安装后在面板右下角手动切换。/Failed to save panel language. Please switch it manually from the lower-left panel control after installation./g' \
-e 's/正在使用 apk 安装依赖.../Installing dependencies with apk.../g' \
-e 's/正在使用 apt 安装依赖.../Installing dependencies with apt.../g' \
-e 's/正在使用 dnf 安装依赖.../Installing dependencies with dnf.../g' \
-e 's/正在使用 yum 安装依赖.../Installing dependencies with yum.../g' \
-e 's/依赖安装后仍未找到/Still missing after dependency installation: /g' \
-e 's/,请检查 LXC 软件源\/安装日志。/. Check the LXC repository\/install log./g' \
-e 's/,请检查系统网络工具包。/. Check the system network tools package./g' \
-e 's/,请检查 iproute2 安装。/. Check the iproute2 installation./g' \
-e 's/,请检查 libvirt-client\/libvirt-clients 安装。/. Check the libvirt-client\/libvirt-clients installation./g' \
-e 's/,请检查 qemu-utils\/qemu-img 安装。/. Check the qemu-utils\/qemu-img installation./g' \
-e 's/,请检查 cloud-image-utils\/cloud-utils 安装。/. Check the cloud-image-utils\/cloud-utils installation./g' \
-e 's/可选依赖未安装:/Optional dependency was not installed: /g' \
-e 's/当前系统 /Current system /g' \
-e 's/ 未找到 dnf\/yum,无法安装依赖。/ does not have dnf\/yum; cannot install dependencies./g' \
-e 's/Windows KVM 初始化需要 genisoimage、mkisofs 或 xorriso 中任意一个。/Windows KVM initialization requires one of genisoimage, mkisofs, or xorriso./g' \
-e 's/未检测到 \/dev\/kvm。LXC 可用,但 KVM 虚拟机需要硬件虚拟化或嵌套虚拟化。/\/dev\/kvm was not detected. LXC is available, but KVM VMs require hardware virtualization or nested virtualization./g' \
-e 's/正在启用内核转发配置.../Enabling kernel forwarding settings.../g' \
-e 's/正在配置 LXC 和 KVM 服务.../Configuring LXC and KVM services.../g' \
-e 's/服务 /Service /g' \
-e 's/ 启动失败,将继续安装并在运行时降级处理。/ failed to start; installation will continue and runtime fallback will be used./g' \
-e 's/未检测到 systemd 单元 /systemd unit was not detected: /g' \
-e 's/,跳过。/; skipped./g' \
-e 's/检测到 libvirt 传统 libvirtd 服务,已使用 libvirtd 模式。/Detected the legacy libvirt libvirtd service; using libvirtd mode./g' \
-e 's/未检测到支持的服务管理器。CLICD 当前支持 systemd 或 OpenRC。/No supported service manager was detected. CLICD currently supports systemd or OpenRC./g' \
-e 's/正在检查 libvirt default NAT 网络.../Checking libvirt default NAT network.../g' \
-e 's/未找到 virsh,跳过 libvirt default NAT 网络检查。/virsh was not found; skipping the libvirt default NAT network check./g' \
-e 's/libvirt default 网络仍未启动。请执行 virsh net-info default 查看详情。/libvirt default network is still not active. Run virsh net-info default for details./g' \
-e 's/libvirt default NAT 网络已启用。/libvirt default NAT network is enabled./g' \
-e 's/正在配置 subordinate UID\/GID 范围.../Configuring subordinate UID\/GID ranges.../g' \
-e 's/根文件系统 /Root filesystem /g' \
-e 's/ 不需要\/不适合自动启用 ext4 project quotaCLICD 将使用兼容磁盘限制模式。/ does not need or is not suitable for automatic ext4 project quota; CLICD will use compatible disk limit mode./g' \
-e 's/ 不在自动 project quota 支持范围,CLICD 将使用兼容磁盘限制模式。/ is not supported for automatic project quota; CLICD will use compatible disk limit mode./g' \
-e 's/根分区来源 /Root partition source /g' \
-e 's/ 不是块设备,跳过 project quota 自动检查,CLICD 将使用兼容磁盘限制模式。/ is not a block device; skipping automatic project quota check and using compatible disk limit mode./g' \
-e 's/未找到 tune2fs,跳过 project quota 检查,CLICD 将使用兼容磁盘限制模式。/tune2fs was not found; skipping project quota check and using compatible disk limit mode./g' \
-e 's/检测到 ext4 project quota 已可用。/ext4 project quota is already available./g' \
-e 's/ext4 project quota 未启用,CLICD 将自动回退到 loopback 镜像磁盘限制模式。/ext4 project quota is not enabled; CLICD will automatically fall back to loopback image disk limit mode./g' \
-e 's/当前目录未找到 clicd 二进制,将下载发行版包。/No local clicd binary found; downloading release package./g' \
-e 's/正在下载发行版包:/Downloading release package: /g' \
-e 's/下载发行版包需要 curl 或 wget。/Downloading the release package requires curl or wget./g' \
-e 's/下载的发行版包中未找到 clicd 二进制。/The downloaded release package does not contain the clicd binary./g' \
-e 's/未找到 clicd 二进制,安装无法继续。/clicd binary was not found; installation cannot continue./g' \
-e 's/已安装二进制:/Installed binary: /g' \
-e 's/正在安装 CLICD 服务.../Installing CLICD service.../g' \
-e 's/正在清理 CLICD 防火墙和网桥规则.../Cleaning CLICD firewall and bridge rules.../g' \
-e 's/已清理 /Cleaned /g' \
-e 's/ 中的 CLICD 配额记录/ CLICD quota records/g' \
-e 's/跳过当前安装目录 /Skipping current installation directory /g' \
-e 's/,避免中断后续安装步骤。/ to avoid interrupting later installation steps./g' \
-e 's/ 被占用,终止占用进程后重试删除.../ is busy; killing occupying processes and retrying removal.../g' \
-e 's/正在删除 CLICD 使用的 LXC 镜像缓存.../Removing LXC image cache used by CLICD.../g' \
-e 's/正在删除 KVM 虚拟机域 /Removing KVM VM domain /g' \
-e 's/正在销毁 CLICD 创建的 KVM 虚拟机.../Destroying CLICD-created KVM VMs.../g' \
-e 's/检测到非 CLICD 虚拟机仍在使用 libvirt default 网络,已保留 default\/virbr0。/Non-CLICD VMs are still using the libvirt default network, so default\/virbr0 has been kept./g' \
-e 's/正在删除 CLICD 创建的 libvirt default NAT 网络.../Removing CLICD-created libvirt default NAT network.../g' \
-e 's/已删除 /Removed /g' \
-e 's/检测到 /Detected /g' \
-e 's/安装完成/Installation complete/g' \
-e 's/Web 面板/Web panel/g' \
-e 's/二进制/Binary/g' \
-e 's/安装日志/Install log/g' \
-e 's/服务/Service/g' \
-e 's/运行日志/Runtime log/g' \
-e 's/首次安装时的初始账号信息:/Initial account information for first installation:/g' \
-e 's/如果没有显示密码,说明服务器已有/If no password is shown, the server already has/g' \
-e 's/已有管理员密码使用 bcrypt 存储,无法反查;请使用面板内修改密码或重置配置。/Existing admin passwords are stored with bcrypt and cannot be recovered. Change it in the panel or reset configuration./g' \
-e 's//: /g' \
-e 's//, /g' \
-e 's/。/./g' \
-e 's//(/g' \
-e 's//)/g' \
-e 's/、/, /g' \
)"
printf '%s' "$msg"
}
echo "====================================="
echo " $(tr_msg "CLICD 中文安装/卸载脚本")"
echo "====================================="
write_log_file() {
if [ "$(id -u 2>/dev/null || echo 1)" = "0" ]; then
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)" "$*" >> "$LOG_FILE" 2>/dev/null || true
fi
}
log() {
msg="$(tr_msg "$*")"
echo "[clicd] $msg"
write_log_file "[clicd] $msg"
}
warn() {
label="$(tr_msg "警告")"
msg="$(tr_msg "$*")"
echo "[clicd][$label] $msg" >&2
write_log_file "[$label] $msg"
}
die() {
label="$(tr_msg "错误")"
msg="$(tr_msg "$*")"
echo "[clicd][$label] $msg" >&2
write_log_file "[$label] $msg"
echo "" >&2
echo "$(tr_msg "安装/卸载未完成。请查看日志:")$LOG_FILE" >&2
echo "$(tr_msg "如果你确认这是程序问题,请提交 issue:")$ISSUE_URL" >&2
exit 1
}
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
is_systemd() {
has_cmd systemctl && [ -d /run/systemd/system ]
}
is_openrc() {
has_cmd rc-service && has_cmd rc-update
}
run_step() {
step_name="$1"
shift
log "开始:$step_name"
if ( "$@" ) >> "$LOG_FILE" 2>&1; then
log "完成:$step_name"
return 0
fi
rc="$?"
echo "" >&2
echo "[clicd][$(tr_msg "错误")] $(tr_msg "步骤失败:")$(tr_msg "$step_name")$(tr_msg "")$(tr_msg "退出码:")$rc" >&2
echo "[clicd][$(tr_msg "错误")] $(tr_msg "最近 80 行日志:")$LOG_FILE" >&2
tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true
echo "" >&2
echo "$(tr_msg "请将上述日志和系统信息提交到:")$ISSUE_URL" >&2
exit "$rc"
}
check_os_compatibility() {
log "系统检测:ID=${OS_ID} ID_LIKE=${OS_LIKE} ARCH=$(uname -m 2>/dev/null || echo unknown)"
case "$(uname -m 2>/dev/null || echo unknown)" in
x86_64|amd64)
;;
*)
die "当前安装包仅支持 x86_64/amd64,当前架构:$(uname -m 2>/dev/null || echo unknown)。"
;;
esac
if ! is_systemd && ! is_openrc; then
die "未检测到 systemd 或 OpenRC,无法安装服务。"
fi
case "$OS_ID" in
ubuntu|debian|alpine|centos|rhel|rocky|almalinux|fedora)
;;
*)
if ! has_cmd apt-get && ! has_cmd apk && ! has_cmd dnf && ! has_cmd yum; then
die "暂不支持当前 Linux 发行版:${OS_ID} ${OS_LIKE}。请提交 issue 并附上 /etc/os-release。"
fi
warn "发行版 ${OS_ID} 不在主要支持列表,将按检测到的软件包管理器尝试安装。"
;;
esac
}
check_storage_compatibility() {
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || echo unknown)"
avail_kb="$(df -Pk / 2>/dev/null | awk 'NR==2 {print $4}' || echo 0)"
log "存储检测:根文件系统=${root_fs} 可用空间=${avail_kb}KB"
if [ "${avail_kb:-0}" -lt 5242880 ]; then
warn "根分区可用空间低于 5GB,下载镜像或创建 KVM/LXC 时可能失败。"
fi
}
if [ "$(id -u)" -ne 0 ]; then
echo "$(tr_msg "请使用 root 权限运行:")sudo ./install.sh"
echo "$(tr_msg "或执行:")curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh"
echo "$(tr_msg "卸载:")curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall"
echo "$(tr_msg "问题反馈:")$ISSUE_URL"
exit 1
fi
: > "$LOG_FILE" 2>/dev/null || true
log "日志文件:$LOG_FILE"
log "仓库地址:https://github.com/${REPO}"
log "问题反馈:$ISSUE_URL"
OS_ID="unknown"
OS_LIKE=""
if [ -r /etc/os-release ]; then
. /etc/os-release
OS_ID="${ID:-unknown}"
OS_LIKE="${ID_LIKE:-}"
fi
usage() {
if [ "$CLICD_LANG_DETECTED" = "en" ]; then
cat << EOF
Usage:
./install.sh Install or upgrade CLICD
./install.sh uninstall Uninstall CLICD (removes containers, VMs, image cache, and config data)
Environment variables:
CLICD_REPO=owner/repo Default: ${REPO}
CLICD_VERSION=latest|v1.0.0 Default: latest
CLICD_LANG=en|zh Default: auto
CLICD_LOG_FILE=/path/file.log Default: ${LOG_FILE}
Examples:
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall --yes
Log: ${LOG_FILE}
Issues: ${ISSUE_URL}
EOF
return
fi
cat << EOF
用法:
./install.sh 安装或升级 CLICD
./install.sh uninstall 卸载 CLICD(会删除容器、虚拟机、镜像缓存和配置数据)
环境变量:
CLICD_REPO=owner/repo 默认:${REPO}
CLICD_VERSION=latest|v1.0.0 默认:latest
CLICD_LANG=en|zh 默认:自动检测
CLICD_LOG_FILE=/path/file.log 默认:${LOG_FILE}
示例:
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall --yes
日志:${LOG_FILE}
问题反馈:${ISSUE_URL}
EOF
}
remove_path() {
path="$1"
if [ ! -e "$path" ] && [ ! -L "$path" ]; then
return
fi
rm -rf "$path"
log "已删除 $path"
}
unmount_path_tree() {
path="$1"
if [ ! -e "$path" ]; then
return
fi
if has_cmd findmnt; then
findmnt -R -n -o TARGET "$path" 2>/dev/null | sort -r | while IFS= read -r mountpoint; do
[ -n "$mountpoint" ] || continue
umount -R -l "$mountpoint" >/dev/null 2>&1 || umount -l "$mountpoint" >/dev/null 2>&1 || true
done
fi
umount -R -l "$path/rootfs" >/dev/null 2>&1 || umount -l "$path/rootfs" >/dev/null 2>&1 || true
umount -R -l "$path" >/dev/null 2>&1 || umount -l "$path" >/dev/null 2>&1 || true
}
detach_container_loop_devices() {
path="$1"
if ! has_cmd losetup; then
return
fi
for image in "$path"/rootfs.img "$path"/*.img; do
[ -e "$image" ] || continue
losetup -j "$image" 2>/dev/null | sed 's/:.*//' | while IFS= read -r loopdev; do
[ -n "$loopdev" ] || continue
losetup -d "$loopdev" >/dev/null 2>&1 || true
done
done
}
kill_path_users() {
path="$1"
if has_cmd fuser && [ -e "$path" ]; then
fuser -km "$path" >/dev/null 2>&1 || true
fi
}
remove_lxc_container_dir() {
container_dir="$1"
container_name="$(basename "$container_dir")"
if has_cmd lxc-stop; then
lxc-stop -n "$container_name" -k >/dev/null 2>&1 || true
fi
if has_cmd lxc-destroy; then
lxc-destroy -n "$container_name" -f >/dev/null 2>&1 || true
fi
unmount_path_tree "$container_dir"
detach_container_loop_devices "$container_dir"
if rm -rf "$container_dir" >/dev/null 2>&1; then
log "已删除 $container_dir"
return
fi
log "检测到 $container_dir 被占用,终止占用进程后重试删除..."
kill_path_users "$container_dir/rootfs"
kill_path_users "$container_dir"
unmount_path_tree "$container_dir"
detach_container_loop_devices "$container_dir"
rm -rf "$container_dir"
log "已删除 $container_dir"
}
remove_clicd_lxc_image_cache() {
log "正在删除 CLICD 使用的 LXC 镜像缓存..."
for container_dir in /var/lib/lxc/clicd-img-dl-*; do
[ -d "$container_dir" ] || continue
remove_lxc_container_dir "$container_dir"
done
for image in \
"ubuntu noble amd64" \
"ubuntu jammy amd64" \
"debian bookworm amd64" \
"debian bullseye amd64" \
"alpine 3.21 amd64" \
"centos 9-Stream amd64" \
"archlinux current amd64" \
"fedora 44 amd64" \
"rockylinux 10 amd64"
do
set -- $image
distro="$1"
release="$2"
arch="$3"
cache_dir="/var/cache/lxc/download/$distro/$release/$arch"
remove_path "$cache_dir"
rmdir "/var/cache/lxc/download/$distro/$release" >/dev/null 2>&1 || true
rmdir "/var/cache/lxc/download/$distro" >/dev/null 2>&1 || true
done
rmdir /var/cache/lxc/download >/dev/null 2>&1 || true
rmdir /var/cache/lxc >/dev/null 2>&1 || true
}
remove_kvm_domain() {
domain="$1"
case "$domain" in
vm-[0-9]*)
;;
*)
return
;;
esac
suffix="${domain#vm-}"
case "$suffix" in
""|*[!0-9]*)
return
;;
esac
if [ ! -d "/var/lib/clicd/kvm/instances/$domain" ] &&
! virsh dumpxml "$domain" 2>/dev/null | grep -q '/var/lib/clicd/kvm/'; then
return
fi
log "正在删除 KVM 虚拟机域 $domain..."
virsh destroy "$domain" >/dev/null 2>&1 || true
virsh undefine "$domain" --remove-all-storage --nvram >/dev/null 2>&1 ||
virsh undefine "$domain" --nvram >/dev/null 2>&1 ||
virsh undefine "$domain" >/dev/null 2>&1 ||
true
}
destroy_clicd_kvm_domains() {
if ! has_cmd virsh; then
return
fi
log "正在销毁 CLICD 创建的 KVM 虚拟机..."
virsh list --all --name 2>/dev/null | while IFS= read -r domain; do
[ -n "$domain" ] || continue
remove_kvm_domain "$domain"
done
}
domain_is_clicd_kvm() {
domain="$1"
case "$domain" in
vm-[0-9]*)
return 0
;;
esac
virsh dumpxml "$domain" 2>/dev/null | grep -q '/var/lib/clicd/kvm/'
}
libvirt_default_used_by_non_clicd_domain() {
if ! has_cmd virsh; then
return 1
fi
for domain in $(virsh list --all --name 2>/dev/null); do
[ -n "$domain" ] || continue
if domain_is_clicd_kvm "$domain"; then
continue
fi
if virsh domiflist "$domain" 2>/dev/null | awk '$3 == "default" || $3 == "virbr0" {found = 1} END {exit found ? 0 : 1}'; then
return 0
fi
done
return 1
}
remove_clicd_libvirt_default_network() {
if ! has_cmd virsh || [ ! -f "$LIBVIRT_DEFAULT_MARKER" ]; then
return
fi
if libvirt_default_used_by_non_clicd_domain; then
warn "检测到非 CLICD 虚拟机仍在使用 libvirt default 网络,已保留 default/virbr0。"
return
fi
log "正在删除 CLICD 创建的 libvirt default NAT 网络..."
virsh net-destroy default >/dev/null 2>&1 || true
virsh net-undefine default >/dev/null 2>&1 || true
rm -f "$LIBVIRT_DEFAULT_MARKER"
}
delete_iptables_lines() {
table="$1"
chain="$2"
pattern="$3"
if ! has_cmd iptables; then
return
fi
while :; do
line="$(iptables -t "$table" -L "$chain" -n --line-numbers 2>/dev/null | awk -v pat="$pattern" '$0 ~ pat {print $1; exit}')"
[ -n "$line" ] || break
iptables -t "$table" -D "$chain" "$line" >/dev/null 2>&1 || break
done
}
delete_iptables_rule() {
table="$1"
shift
if ! has_cmd iptables; then
return
fi
while iptables -t "$table" -D "$@" >/dev/null 2>&1; do
:
done
}
delete_filter_rule() {
if ! has_cmd iptables; then
return
fi
while iptables -D "$@" >/dev/null 2>&1; do
:
done
}
delete_ip6_filter_rule() {
if ! has_cmd ip6tables; then
return
fi
while ip6tables -D "$@" >/dev/null 2>&1; do
:
done
}
delete_ip6tables_nat_source() {
source="$1"
if ! has_cmd ip6tables || [ -z "$source" ]; then
return
fi
while :; do
rule="$(
ip6tables -t nat -S POSTROUTING 2>/dev/null |
grep -F -- "-s $source" |
grep -F -- " -j MASQUERADE" |
sed 's/^-A /-D /' |
head -n 1
)"
[ -n "$rule" ] || break
# shellcheck disable=SC2086
ip6tables -t nat $rule >/dev/null 2>&1 || break
done
}
read_clicd_network_records() {
db="/root/.clicd/config.db"
legacy="/root/.clicd/config.json"
query="SELECT COALESCE(virtualization,''), COALESCE(ipv6,''), COALESCE(ipv6_interface,''), COALESCE(mac_address,'') FROM containers WHERE COALESCE(ipv6,'') <> '' OR COALESCE(mac_address,'') <> '';"
if [ -f "$db" ] && has_cmd sqlite3; then
sqlite3 -separator '|' "$db" "$query" 2>/dev/null || true
elif [ -f "$db" ] && has_cmd python3; then
CLICD_DB="$db" python3 - <<'PY' 2>/dev/null || true
import os
import sqlite3
db = os.environ.get("CLICD_DB")
for row in sqlite3.connect(db).execute(
"SELECT COALESCE(virtualization,''), COALESCE(ipv6,''), COALESCE(ipv6_interface,''), COALESCE(mac_address,'') "
"FROM containers WHERE COALESCE(ipv6,'') <> '' OR COALESCE(mac_address,'') <> ''"
):
print("|".join("" if value is None else str(value) for value in row))
PY
fi
if [ -f "$legacy" ] && has_cmd python3; then
CLICD_LEGACY_CONFIG="$legacy" python3 - <<'PY' 2>/dev/null || true
import json
import os
path = os.environ.get("CLICD_LEGACY_CONFIG")
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
for item in data.get("containers", []):
virt = item.get("virtualization", "")
ipv6 = item.get("ipv6", "")
uplink = item.get("ipv6_interface", "")
mac = item.get("mac_address", "")
if ipv6 or mac:
print("|".join(str(value or "") for value in (virt, ipv6, uplink, mac)))
PY
fi
}
cleanup_clicd_ipv6_record() {
virt="$1"
ipv6="$2"
uplink="$3"
mac="$4"
bridge="lxcbr0"
if [ "$virt" = "kvm" ]; then
bridge="virbr0"
fi
mac="$(printf '%s' "$mac" | tr '[:upper:]' '[:lower:]')"
if [ -n "$mac" ] && [ "$bridge" = "virbr0" ]; then
delete_ip6_filter_rule FORWARD -i "$bridge" -m mac --mac-source "$mac" -j DROP
fi
[ -n "$ipv6" ] || return
addr="${ipv6%%/*}"
source="$ipv6"
case "$source" in
*/*) ;;
*) source="$source/128" ;;
esac
delete_ip6tables_nat_source "$source"
delete_ip6_filter_rule FORWARD -i "$bridge" -s "$source" -j ACCEPT
delete_ip6_filter_rule FORWARD -o "$bridge" -d "$source" -j ACCEPT
if [ -n "$mac" ] && [ "$bridge" = "virbr0" ]; then
delete_ip6_filter_rule FORWARD -i "$bridge" -m mac --mac-source "$mac" -s "$source" -j ACCEPT
delete_ip6_filter_rule FORWARD -i "$bridge" -m mac --mac-source "$mac" -j DROP
fi
if has_cmd ip; then
ip -6 route del "$source" dev "$bridge" >/dev/null 2>&1 || true
if [ -n "$uplink" ]; then
ip -6 neigh del proxy "$addr" dev "$uplink" >/dev/null 2>&1 || true
fi
fi
}
cleanup_clicd_ipv6_from_config() {
read_clicd_network_records | while IFS='|' read -r virt ipv6 uplink mac; do
cleanup_clicd_ipv6_record "$virt" "$ipv6" "$uplink" "$mac"
done
}
cleanup_clicd_ipv6_bridge_routes() {
if ! has_cmd ip; then
return
fi
for bridge in lxcbr0 virbr0; do
ip -6 route show dev "$bridge" 2>/dev/null | awk '$1 ~ /\/128$/ {print $1}' | while IFS= read -r source; do
[ -n "$source" ] || continue
addr="${source%%/*}"
delete_ip6tables_nat_source "$source"
delete_ip6_filter_rule FORWARD -i "$bridge" -s "$source" -j ACCEPT
delete_ip6_filter_rule FORWARD -o "$bridge" -d "$source" -j ACCEPT
ip -6 neigh show proxy 2>/dev/null | awk -v addr="$addr" '$1 == addr {for (i = 1; i < NF; i++) if ($i == "dev") print $(i + 1)}' | while IFS= read -r uplink; do
[ -n "$uplink" ] || continue
ip -6 neigh del proxy "$addr" dev "$uplink" >/dev/null 2>&1 || true
done
ip -6 route del "$source" dev "$bridge" >/dev/null 2>&1 || true
done
ip -6 addr del fe80::1/64 dev "$bridge" >/dev/null 2>&1 || true
done
}
delete_ip6tables_bridge_rules() {
if ! has_cmd ip6tables; then
return
fi
for bridge in lxcbr0 virbr0; do
while :; do
rule="$(ip6tables -S FORWARD 2>/dev/null | grep -- "$bridge" | sed 's/^-A /-D /' | head -n 1)"
[ -n "$rule" ] || break
# shellcheck disable=SC2086
ip6tables $rule >/dev/null 2>&1 || break
done
done
}
cleanup_clicd_networking() {
log "正在清理 CLICD 防火墙和网桥规则..."
delete_iptables_lines nat PREROUTING 'clicd-'
delete_iptables_rule nat POSTROUTING -s 10.0.3.0/24 -o eth+ -j MASQUERADE
delete_iptables_rule nat POSTROUTING -s 192.168.122.0/24 -o eth+ -j MASQUERADE
cleanup_clicd_ipv6_from_config
cleanup_clicd_ipv6_bridge_routes
for bridge in lxcbr0 virbr0; do
delete_filter_rule FORWARD -i "$bridge" -j ACCEPT
delete_filter_rule FORWARD -o "$bridge" -j ACCEPT
delete_filter_rule FORWARD -i "$bridge" -o "$bridge" -j ACCEPT
done
delete_ip6tables_bridge_rules
}
remove_clicd_host_hooks() {
if has_cmd systemctl; then
systemctl stop clicd-kvm-ipv6.service >/dev/null 2>&1 || true
systemctl disable clicd-kvm-ipv6.service >/dev/null 2>&1 || true
fi
if has_cmd rc-service; then
rc-service clicd-kvm-ipv6 stop >/dev/null 2>&1 || true
fi
if has_cmd rc-update; then
rc-update del clicd-kvm-ipv6 default >/dev/null 2>&1 || true
fi
remove_path /usr/local/sbin/clicd-kvm-ipv6-init
remove_path /etc/systemd/system/clicd-kvm-ipv6.service
remove_path /etc/local.d/clicd-kvm-ipv6.start
remove_path /etc/network/if-up.d/clicd-kvm-ipv6
}
remove_clicd_quota_records() {
for file in /etc/projects /etc/projid; do
[ -f "$file" ] || continue
tmp="${file}.clicd-clean.$$"
grep -v 'clicd-' "$file" > "$tmp" || true
cat "$tmp" > "$file"
rm -f "$tmp"
log "已清理 $file 中的 CLICD 配额记录"
done
}
remove_clicd_tmp_files() {
current_dir="$(pwd -P 2>/dev/null || pwd)"
for path in /tmp/clicd-* /tmp/clicd.*; do
[ -e "$path" ] || [ -L "$path" ] || continue
abs_path="$(cd "$(dirname "$path")" 2>/dev/null && pwd -P)/$(basename "$path")"
if [ "$abs_path" = "$current_dir" ]; then
log "跳过当前安装目录 $path,避免中断后续安装步骤。"
continue
fi
rm -rf "$path"
log "已删除 $path"
done
}
remove_clicd_swapfile() {
if [ ! -e /swapfile ]; then
return
fi
swapoff /swapfile >/dev/null 2>&1 || true
remove_path /swapfile
}
confirm_uninstall() {
if [ "${CLICD_UNINSTALL_CONFIRM:-}" = "1" ] || [ "${CLICD_UNINSTALL_CONFIRM:-}" = "yes" ] || [ "$ACTION_CONFIRM" = "--yes" ] || [ "$ACTION_CONFIRM" = "-y" ]; then
return
fi
echo ""
echo "[clicd][$(tr_msg "警告")] $(tr_msg "卸载会停止并删除 CLICD 服务、配置数据库、CLICD 创建的 LXC/KVM 实例和缓存数据。")" >&2
echo "[clicd][$(tr_msg "警告")] $(tr_msg "为避免误删生产数据,脚本只会删除名称形如 ct-数字 的 LXC 容器、clicd-img-dl-* 下载临时容器和 vm-数字 的 KVM 域。")" >&2
echo "$(tr_msg "如需确认卸载,请输入:YES")" >&2
if [ -r /dev/tty ]; then
IFS= read -r answer < /dev/tty
elif [ -t 0 ]; then
IFS= read -r answer
else
answer=""
fi
if [ "$answer" != "YES" ]; then
die "已取消卸载。如需非交互卸载,请设置 CLICD_UNINSTALL_CONFIRM=1。"
fi
}
uninstall_clicd() {
confirm_uninstall
log "正在卸载 CLICD..."
if has_cmd systemctl; then
systemctl stop clicd >/dev/null 2>&1 || true
systemctl disable clicd >/dev/null 2>&1 || true
fi
if has_cmd rc-service; then
rc-service clicd stop >/dev/null 2>&1 || true
fi
if has_cmd rc-update; then
rc-update del clicd default >/dev/null 2>&1 || true
fi
log "正在删除 CLICD 创建的 LXC 容器(/var/lib/lxc/ct-数字)..."
for container_dir in /var/lib/lxc/ct-[0-9]*; do
[ -d "$container_dir" ] || continue
remove_lxc_container_dir "$container_dir"
done
remove_clicd_lxc_image_cache
destroy_clicd_kvm_domains
remove_clicd_libvirt_default_network
cleanup_clicd_networking
remove_clicd_host_hooks
remove_clicd_quota_records
remove_path /etc/systemd/system/clicd.service
remove_path /etc/init.d/clicd
remove_path /usr/local/bin/clicd
remove_path /etc/sysctl.d/99-clicd.conf
remove_path /var/log/clicd.log
remove_path /var/log/clicd.err
remove_path /root/.clicd
# /var/lib/lxc 可能包含非 CLICD 容器,生产环境不整体删除。
unmount_path_tree /var/lib/clicd
remove_path /var/lib/clicd
# /var/cache/lxc 是 LXC 全局缓存,已按 CLICD 模板精确清理,生产环境不整体删除。
remove_path /var/cache/clicd
warn "保留 /root/clicd-backups,避免误删部署/回滚备份。确认不需要后可手动删除。"
remove_clicd_tmp_files
remove_clicd_swapfile
if has_cmd systemctl; then
systemctl daemon-reload >/dev/null 2>&1 || true
systemctl reset-failed clicd >/dev/null 2>&1 || true
fi
if has_cmd sysctl; then
sysctl --system >/dev/null 2>&1 || true
fi
echo ""
echo "====================================="
echo " $(tr_msg "CLICD 卸载完成")"
echo "====================================="
echo " $(tr_msg "已删除服务、二进制、SQLite/配置数据、CLICD LXC/KVM 实例、")"
echo " $(tr_msg "CLICD 镜像缓存、防火墙规则、主机钩子、配额记录和临时文件。")"
echo " $(tr_msg "已保留 /root/clicd-backups 和非 CLICD 的 LXC 全局缓存,避免误删生产备份/共享镜像。")"
echo " $(tr_msg "日志:")$LOG_FILE"
echo " $(tr_msg "问题反馈:")$ISSUE_URL"
echo "====================================="
}
case "$ACTION" in
install|"")
;;
uninstall|remove)
uninstall_clicd
exit 0
;;
-h|--help|help)
usage
exit 0
;;
*)
die "未知操作:$ACTION"
;;
esac
install_apk() {
log "正在使用 apk 安装依赖..."
apk update
apk add --no-cache \
ca-certificates \
curl \
wget \
tar \
gzip \
xz \
lxc \
lxc-download \
lxc-openrc \
lxc-bridge \
lxc-templates \
bridge-utils \
iproute2 \
iptables \
dnsmasq \
dbus \
qemu-system-x86_64 \
qemu-img \
libvirt \
libvirt-daemon \
libvirt-client \
libvirt-qemu
for pkg in lxcfs shadow conntrack-tools quota-tools e2fsprogs xfsprogs cloud-utils genisoimage xorriso smartmontools; do
apk add --no-cache "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg"
done
}
install_apt() {
log "正在使用 apt 安装依赖..."
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y \
ca-certificates \
curl \
wget \
tar \
gzip \
xz-utils \
lxc \
lxc-templates \
lxcfs \
bridge-utils \
uidmap \
iproute2 \
iptables \
conntrack \
quota \
e2fsprogs \
xfsprogs \
dnsmasq-base \
qemu-kvm \
qemu-system-x86 \
qemu-utils \
libvirt-daemon-system \
libvirt-clients \
cloud-image-utils \
genisoimage \
xorriso \
smartmontools \
virtinst \
ovmf
}
enable_el_repos() {
if has_cmd dnf; then
dnf install -y 'dnf-command(config-manager)' >/dev/null 2>&1 || true
dnf install -y epel-release || true
dnf config-manager --set-enabled crb >/dev/null 2>&1 || true
dnf config-manager --set-enabled powertools >/dev/null 2>&1 || true
elif has_cmd yum; then
yum install -y yum-utils >/dev/null 2>&1 || true
yum install -y epel-release || true
yum-config-manager --enable powertools >/dev/null 2>&1 || true
fi
}
install_dnf() {
log "正在使用 dnf 安装依赖..."
enable_el_repos
dnf install -y \
ca-certificates \
curl \
wget \
tar \
gzip \
xz \
lxc \
lxc-templates \
bridge-utils \
iproute \
iptables \
conntrack-tools \
shadow-utils \
quota \
e2fsprogs \
xfsprogs \
dnsmasq \
qemu-kvm \
qemu-img \
libvirt \
libvirt-daemon-kvm \
libvirt-client \
virt-install \
cloud-utils \
genisoimage
for pkg in lxcfs xorriso edk2-ovmf smartmontools; do
dnf install -y "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg"
done
}
install_yum() {
log "正在使用 yum 安装依赖..."
enable_el_repos
yum install -y \
ca-certificates \
curl \
wget \
tar \
gzip \
xz \
lxc \
lxc-templates \
bridge-utils \
iproute \
iptables \
conntrack-tools \
shadow-utils \
quota \
e2fsprogs \
xfsprogs \
dnsmasq \
qemu-kvm \
qemu-img \
libvirt \
libvirt-daemon-kvm \
libvirt-client \
virt-install \
cloud-utils \
genisoimage
for pkg in lxcfs xorriso edk2-ovmf smartmontools; do
yum install -y "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg"
done
}
install_dependencies() {
case "$OS_ID" in
ubuntu|debian)
install_apt
;;
alpine)
install_apk
;;
centos|rhel|rocky|almalinux|fedora)
if has_cmd dnf; then
install_dnf
elif has_cmd yum; then
install_yum
else
die "当前系统 $OS_ID 未找到 dnf/yum,无法安装依赖。"
fi
;;
*)
if has_cmd apt-get; then
install_apt
elif has_cmd apk; then
install_apk
elif has_cmd dnf; then
install_dnf
elif has_cmd yum; then
install_yum
else
die "暂不支持当前 Linux 发行版:${OS_ID} ${OS_LIKE}。请提交 issue 并附上 /etc/os-release。"
fi
;;
esac
has_cmd lxc-create || die "依赖安装后仍未找到 lxc-create,请检查 LXC 软件源/安装日志。"
has_cmd iptables || die "依赖安装后仍未找到 iptables,请检查系统网络工具包。"
has_cmd ip || die "依赖安装后仍未找到 ip 命令,请检查 iproute2 安装。"
has_cmd virsh || die "依赖安装后仍未找到 virsh,请检查 libvirt-client/libvirt-clients 安装。"
has_cmd qemu-img || die "依赖安装后仍未找到 qemu-img,请检查 qemu-utils/qemu-img 安装。"
has_cmd cloud-localds || die "依赖安装后仍未找到 cloud-localds,请检查 cloud-image-utils/cloud-utils 安装。"
if ! has_cmd genisoimage && ! has_cmd mkisofs && ! has_cmd xorriso; then
die "Windows KVM 初始化需要 genisoimage、mkisofs 或 xorriso 中任意一个。"
fi
if [ ! -e /dev/kvm ]; then
warn "未检测到 /dev/kvm。LXC 可用,但 KVM 虚拟机需要硬件虚拟化或嵌套虚拟化。"
fi
}
configure_kernel_networking() {
log "正在启用内核转发配置..."
cat > /etc/sysctl.d/99-clicd.conf << 'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.bridge.bridge-nf-call-iptables = 0
net.bridge.bridge-nf-call-ip6tables = 0
EOF
modprobe br_netfilter >/dev/null 2>&1 || true
sysctl --system >/dev/null 2>&1 || true
}
systemd_unit_exists() {
unit="$1"
systemctl list-unit-files "$unit" >/dev/null 2>&1 || [ -e "/etc/systemd/system/$unit" ] || [ -e "/usr/lib/systemd/system/$unit" ] || [ -e "/lib/systemd/system/$unit" ]
}
systemd_enable_now_if_exists() {
unit="$1"
if systemd_unit_exists "$unit"; then
systemctl enable --now "$unit" >/dev/null 2>&1 || warn "服务 $unit 启动失败,将继续安装并在运行时降级处理。"
return
fi
log "未检测到 systemd 单元 $unit,跳过。"
}
systemd_existing_units() {
for unit in "$@"; do
if systemd_unit_exists "$unit"; then
printf ' %s' "$unit"
fi
done
}
setup_runtime_services() {
log "正在配置 LXC 和 KVM 服务..."
if is_systemd; then
systemd_enable_now_if_exists lxcfs.service
systemd_enable_now_if_exists lxc-net.service
systemd_enable_now_if_exists lxc.service
if systemd_unit_exists libvirtd.service; then
systemd_enable_now_if_exists libvirtd.service
log "检测到 libvirt 传统 libvirtd 服务,已使用 libvirtd 模式。"
else
systemd_enable_now_if_exists virtqemud.service
systemd_enable_now_if_exists virtqemud.socket
fi
systemd_enable_now_if_exists virtlogd.socket
return
fi
if is_openrc; then
rc-update add cgroups default >/dev/null 2>&1 || true
rc-service cgroups start >/dev/null 2>&1 || true
rc-update add lxc default >/dev/null 2>&1 || true
rc-service lxc start >/dev/null 2>&1 || true
rc-update add lxcfs default >/dev/null 2>&1 || true
rc-service lxcfs start >/dev/null 2>&1 || true
rc-update add dbus default >/dev/null 2>&1 || true
rc-service dbus start >/dev/null 2>&1 || true
rc-update add libvirtd default >/dev/null 2>&1 || true
rc-service libvirtd start >/dev/null 2>&1 || true
rc-update add virtlogd default >/dev/null 2>&1 || true
rc-service virtlogd start >/dev/null 2>&1 || true
return
fi
die "未检测到支持的服务管理器。CLICD 当前支持 systemd 或 OpenRC。"
}
libvirt_network_active() {
virsh net-info default 2>/dev/null | awk -F: 'tolower($1) ~ /^[[:space:]]*active[[:space:]]*$/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); print tolower($2)}' | grep -qx yes
}
setup_default_libvirt_network() {
if ! has_cmd virsh; then
warn "未找到 virsh,跳过 libvirt default NAT 网络检查。"
return
fi
log "正在检查 libvirt default NAT 网络..."
if ! virsh net-info default >/dev/null 2>&1; then
net_xml="$(mktemp /tmp/clicd-default-net.XXXXXX.xml)"
cat > "$net_xml" << 'EOF'
<network>
<name>default</name>
<bridge name='virbr0'/>
<forward mode='nat'/>
<ip address='192.168.122.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.122.2' end='192.168.122.254'/>
</dhcp>
</ip>
</network>
EOF
virsh net-define "$net_xml"
rm -f "$net_xml"
mkdir -p "$(dirname "$LIBVIRT_DEFAULT_MARKER")"
touch "$LIBVIRT_DEFAULT_MARKER"
fi
if ! libvirt_network_active; then
virsh net-start default
fi
virsh net-autostart default >/dev/null
if ! libvirt_network_active; then
die "libvirt default 网络仍未启动。请执行 virsh net-info default 查看详情。"
fi
log "libvirt default NAT 网络已启用。"
}
setup_subids() {
log "正在配置 subordinate UID/GID 范围..."
touch /etc/subuid /etc/subgid
grep -q '^root:' /etc/subuid 2>/dev/null || echo 'root:100000:65536' >> /etc/subuid
grep -q '^root:' /etc/subgid 2>/dev/null || echo 'root:100000:65536' >> /etc/subgid
}
configure_lxc_storage_access() {
log "Configuring LXC storage directory permissions..."
mkdir -p /var/lib/lxc
chmod 755 /var/lib/lxc
}
try_enable_project_quota() {
root_src="$(findmnt -no SOURCE / 2>/dev/null || true)"
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || true)"
case "$root_fs" in
ext4)
;;
xfs|btrfs|zfs|overlay|unknown|"")
log "根文件系统 ${root_fs:-unknown} 不需要/不适合自动启用 ext4 project quotaCLICD 将使用兼容磁盘限制模式。"
return
;;
*)
log "根文件系统 ${root_fs:-unknown} 不在自动 project quota 支持范围,CLICD 将使用兼容磁盘限制模式。"
return
;;
esac
if [ -z "$root_src" ] || [ ! -b "$root_src" ]; then
log "根分区来源 ${root_src:-unknown} 不是块设备,跳过 project quota 自动检查,CLICD 将使用兼容磁盘限制模式。"
return
fi
if ! has_cmd tune2fs; then
log "未找到 tune2fs,跳过 project quota 检查,CLICD 将使用兼容磁盘限制模式。"
return
fi
if tune2fs -l "$root_src" 2>/dev/null | grep -q 'project'; then
log "检测到 ext4 project quota 已可用。"
return
fi
log "ext4 project quota 未启用,CLICD 将自动回退到 loopback 镜像磁盘限制模式。"
}
download_file() {
url="$1"
dest="$2"
rm -f "$dest"
if has_cmd curl; then
curl -fL --retry 6 --retry-delay 2 --connect-timeout 20 --max-time 600 "$url" -o "$dest"
return
fi
if has_cmd wget; then
wget --tries=6 --timeout=30 --waitretry=2 -O "$dest" "$url"
return
fi
return 127
}
release_api_json() {
api_url="https://api.github.com/repos/${REPO}/releases/latest"
if has_cmd curl; then
curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 --max-time 120 "$api_url" 2>/dev/null || true
return
fi
if has_cmd wget; then
wget -qO- --tries=3 --timeout=30 "$api_url" 2>/dev/null || true
return
fi
}
release_asset_url() {
asset_name="$1"
if [ "$CLICD_INSTALL_VERSION" != "latest" ]; then
printf '%s\n' "https://github.com/${REPO}/releases/download/${CLICD_INSTALL_VERSION}/${asset_name}"
return
fi
api_data="$(release_api_json)"
url="$(printf '%s\n' "$api_data" | sed -n 's/.*"browser_download_url": *"\([^"]*\/'"$asset_name"'\)".*/\1/p' | head -n 1)"
if [ -n "$url" ]; then
printf '%s\n' "$url"
return
fi
tag="$(printf '%s\n' "$api_data" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)"
if [ -n "$tag" ]; then
printf '%s\n' "https://github.com/${REPO}/releases/download/${tag}/${asset_name}"
return
fi
printf '%s\n' "https://github.com/${REPO}/releases/latest/download/${asset_name}"
}
download_release_if_needed() {
if [ -f "./clicd" ]; then
return
fi
if [ "$CLICD_INSTALL_VERSION" = "latest" ]; then
download_url="https://github.com/${REPO}/releases/latest/download/${ASSET}"
else
download_url="https://github.com/${REPO}/releases/download/${CLICD_INSTALL_VERSION}/${ASSET}"
fi
log "当前目录未找到 clicd 二进制,将下载发行版包。"
log "正在下载发行版包:${download_url}"
tmp_dir="$(mktemp -d)"
rm -f "$INSTALL_DOWNLOAD_MARKER"
printf '%s\n' "$tmp_dir" > "$INSTALL_DOWNLOAD_MARKER" || die "Failed to write install temp marker."
if ! has_cmd curl && ! has_cmd wget; then
die "下载发行版包需要 curl 或 wget。"
fi
archive_path="$tmp_dir/$ASSET"
archive_urls="$download_url"
resolved_archive_url="$(release_asset_url "$ASSET")"
if [ "$resolved_archive_url" != "$download_url" ]; then
archive_urls="$archive_urls $resolved_archive_url"
fi
archive_ok=0
for url in $archive_urls; do
[ -n "$url" ] || continue
log "Trying release archive: $url"
if download_file "$url" "$archive_path" && [ -s "$archive_path" ]; then
archive_ok=1
break
fi
warn "Release archive download failed, trying next source: $url"
done
if [ "$archive_ok" = "1" ]; then
tar -xzf "$archive_path" -C "$tmp_dir" || die "Failed to extract release package: $archive_path"
else
binary_asset="clicd-linux-amd64"
if [ "$CLICD_INSTALL_VERSION" = "latest" ]; then
binary_url="https://github.com/${REPO}/releases/latest/download/${binary_asset}"
else
binary_url="https://github.com/${REPO}/releases/download/${CLICD_INSTALL_VERSION}/${binary_asset}"
fi
binary_urls="$binary_url"
resolved_binary_url="$(release_asset_url "$binary_asset")"
if [ "$resolved_binary_url" != "$binary_url" ]; then
binary_urls="$binary_urls $resolved_binary_url"
fi
binary_path="$tmp_dir/$binary_asset"
binary_ok=0
for url in $binary_urls; do
[ -n "$url" ] || continue
log "Trying release binary: $url"
if download_file "$url" "$binary_path" && [ -s "$binary_path" ]; then
mkdir -p "$tmp_dir/clicd-linux-amd64"
cp "$binary_path" "$tmp_dir/clicd-linux-amd64/clicd"
chmod +x "$tmp_dir/clicd-linux-amd64/clicd"
binary_ok=1
break
fi
warn "Release binary download failed, trying next source: $url"
done
[ "$binary_ok" = "1" ] || die "Release package download failed: $download_url"
fi
[ -d "$tmp_dir/clicd-linux-amd64" ] || die "Release package layout is invalid: missing clicd-linux-amd64 directory"
[ -f "$tmp_dir/clicd-linux-amd64/clicd" ] || die "下载的发行版包中未找到 clicd 二进制。"
}
install_binary() {
if has_cmd systemctl; then
systemctl stop clicd >/dev/null 2>&1 || true
fi
if has_cmd rc-service; then
rc-service clicd stop >/dev/null 2>&1 || true
fi
bin_src="./clicd"
download_dir=""
if [ ! -f "$bin_src" ] && [ -f "$INSTALL_DOWNLOAD_MARKER" ]; then
download_dir="$(sed -n '1p' "$INSTALL_DOWNLOAD_MARKER" 2>/dev/null || true)"
if [ -n "$download_dir" ] && [ -f "$download_dir/clicd-linux-amd64/clicd" ]; then
bin_src="$download_dir/clicd-linux-amd64/clicd"
fi
fi
[ -f "$bin_src" ] || die "未找到 clicd 二进制,安装无法继续。"
tmp_bin="/usr/local/bin/clicd.new.$$"
cp "$bin_src" "$tmp_bin"
chmod +x "$tmp_bin"
mv -f "$tmp_bin" /usr/local/bin/clicd
chmod +x /usr/local/bin/clicd
log "已安装二进制:/usr/local/bin/clicd"
if [ -n "$download_dir" ]; then
case "$download_dir" in
/tmp/*)
rm -rf "$download_dir"
;;
esac
rm -f "$INSTALL_DOWNLOAD_MARKER"
fi
}
install_systemd_service() {
libvirt_after="$(systemd_existing_units libvirtd.service virtqemud.service virtqemud.socket virtlogd.socket)"
libvirt_wants="$(systemd_existing_units libvirtd.service virtqemud.socket virtlogd.socket)"
lxc_after="$(systemd_existing_units lxc.service lxcfs.service lxc-net.service)"
cat > /etc/systemd/system/clicd.service << EOF
[Unit]
Description=CLICD - LXC/KVM Container Manager
After=network-online.target${lxc_after}${libvirt_after}
Wants=network-online.target${libvirt_wants}
StartLimitIntervalSec=60
StartLimitBurst=10
[Service]
Type=simple
ExecStart=/usr/local/bin/clicd server
Restart=always
RestartSec=5
LimitNOFILE=1048576
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable clicd
systemctl restart clicd
}
install_openrc_service() {
cat > /etc/init.d/clicd << 'EOF'
#!/sbin/openrc-run
name="CLICD"
description="CLICD - LXC/KVM Container Manager"
command="/usr/local/bin/clicd"
command_args="server"
command_background=true
pidfile="/run/clicd.pid"
output_log="/var/log/clicd.log"
error_log="/var/log/clicd.err"
depend() {
need net
after lxc libvirtd
}
EOF
chmod +x /etc/init.d/clicd
rc-update add clicd default
rc-service clicd restart
}
install_service() {
log "正在安装 CLICD 服务..."
if is_systemd; then
install_systemd_service
elif is_openrc; then
install_openrc_service
else
die "未检测到支持的服务管理器。CLICD 当前支持 systemd 或 OpenRC。"
fi
}
set_panel_language() {
lang="$CLICD_LANG_DETECTED"
db="/root/.clicd/config.db"
if [ "$lang" != "zh" ] && [ "$lang" != "en" ]; then
lang="zh"
fi
saved=0
i=0
while [ ! -f "$db" ] && [ "$i" -lt 20 ]; do
i=$((i + 1))
sleep 1
done
if [ -f "$db" ] && has_cmd python3; then
CLICD_PANEL_LANG="$lang" CLICD_DB="$db" python3 - <<'PY' >/dev/null 2>&1 && saved=1 || saved=0
import os
import sqlite3
db = os.environ["CLICD_DB"]
lang = os.environ["CLICD_PANEL_LANG"]
conn = sqlite3.connect(db)
conn.execute("CREATE TABLE IF NOT EXISTS app_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
conn.execute("INSERT OR REPLACE INTO app_meta(key, value) VALUES('language', ?)", (lang,))
conn.commit()
conn.close()
PY
fi
if [ "$saved" != "1" ] && [ -f "$db" ] && has_cmd sqlite3; then
sqlite3 "$db" "CREATE TABLE IF NOT EXISTS app_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL); INSERT OR REPLACE INTO app_meta(key, value) VALUES('language', '$lang');" >/dev/null 2>&1 && saved=1 || saved=0
fi
if [ "$saved" != "1" ] && has_cmd curl; then
curl -k -fsS -X POST -H 'Content-Type: application/json' -d "{\"language\":\"$lang\"}" "https://127.0.0.1:8999/api/language" >/dev/null 2>&1 && saved=1 || \
curl -fsS -X POST -H 'Content-Type: application/json' -d "{\"language\":\"$lang\"}" "http://127.0.0.1:8999/api/language" >/dev/null 2>&1 && saved=1 || saved=0
fi
if [ "$saved" = "1" ]; then
log "已写入面板语言:$lang"
if has_cmd systemctl && systemctl is-active clicd >/dev/null 2>&1; then
systemctl restart clicd >/dev/null 2>&1 || true
elif has_cmd rc-service; then
rc-service clicd restart >/dev/null 2>&1 || true
fi
else
warn "面板语言写入失败,请安装后在面板右下角手动切换。"
fi
}
print_summary() {
echo ""
echo "====================================="
echo " $(tr_msg "安装完成")"
echo "====================================="
echo " $(tr_msg "Web 面板:")http://YOUR_SERVER_IP:8999"
echo " $(tr_msg "二进制:")/usr/local/bin/clicd"
echo " $(tr_msg "安装日志:")$LOG_FILE"
echo " $(tr_msg "问题反馈:")$ISSUE_URL"
if is_systemd; then
echo " $(tr_msg "服务:")systemctl {start|stop|restart|status} clicd"
echo " $(tr_msg "运行日志:")journalctl -u clicd -f"
elif is_openrc; then
echo " $(tr_msg "服务:")rc-service clicd {start|stop|restart|status}"
echo " $(tr_msg "运行日志:")tail -f /var/log/clicd.log /var/log/clicd.err"
fi
echo "====================================="
echo ""
echo "$(tr_msg "首次安装时的初始账号信息:")"
if is_systemd; then
journalctl -u clicd --no-pager -n 80 | grep -E "Username:|Password:" || true
else
grep -E "Username:|Password:" /var/log/clicd.log /var/log/clicd.err 2>/dev/null || true
fi
echo ""
echo "$(tr_msg "如果没有显示密码,说明服务器已有") /root/.clicd/config.db."
echo "$(tr_msg "已有管理员密码使用 bcrypt 存储,无法反查;请使用面板内修改密码或重置配置。")"
}
run_step "兼容性检查" check_os_compatibility
run_step "存储环境检查" check_storage_compatibility
run_step "安装系统依赖" install_dependencies
run_step "配置内核网络参数" configure_kernel_networking
run_step "配置运行时服务" setup_runtime_services
run_step "配置 libvirt default NAT 网络" setup_default_libvirt_network
run_step "配置 UID/GID 映射" setup_subids
run_step "配置 LXC 存储权限" configure_lxc_storage_access
run_step "检查 project quota" try_enable_project_quota
run_step "下载发行版包" download_release_if_needed
run_step "安装 CLICD 二进制" install_binary
run_step "安装并启动 CLICD 服务" install_service
run_step "写入面板语言" set_panel_language
sleep 2
print_summary