diff --git a/backend/internal/kvm/kvm.go b/backend/internal/kvm/kvm.go index 403c4c9..07bccf7 100644 --- a/backend/internal/kvm/kvm.go +++ b/backend/internal/kvm/kvm.go @@ -1407,7 +1407,7 @@ func ensureDefaultNetwork() error { } // Start and autostart the default network if out, err := exec.Command("virsh", "net-info", "default").Output(); err == nil { - if !strings.Contains(strings.ToLower(string(out)), "active:") || !strings.Contains(strings.ToLower(string(out)), "yes") { + if !libvirtNetworkActive(string(out)) { if startOut, startErr := exec.Command("virsh", "net-start", "default").CombinedOutput(); startErr != nil { return fmt.Errorf("failed to start libvirt default network: %v, output: %s", startErr, string(startOut)) } @@ -1419,6 +1419,19 @@ func ensureDefaultNetwork() error { return nil } +func libvirtNetworkActive(info string) bool { + for _, line := range strings.Split(info, "\n") { + key, value, ok := strings.Cut(line, ":") + if !ok { + continue + } + if strings.EqualFold(strings.TrimSpace(key), "Active") { + return strings.EqualFold(strings.TrimSpace(value), "yes") + } + } + return false +} + func createOverlayDisk(base, target string, diskGB int) error { if diskGB < 1 { diskGB = 5 diff --git a/backend/internal/version/version.go b/backend/internal/version/version.go index fae9577..d5862d3 100644 --- a/backend/internal/version/version.go +++ b/backend/internal/version/version.go @@ -1,7 +1,7 @@ package version var ( - Version = "1.1.0" + Version = "1.1.1" Repo = "MengMengCode/CLICD" ) @@ -11,4 +11,3 @@ func Current() string { } return Version } - diff --git a/frontend/package.json b/frontend/package.json index c49564c..2ef6def 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "clicd-frontend", "private": true, - "version": "1.1.0", + "version": "1.1.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 135559c..b9517d5 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -106,7 +106,7 @@ export default function Login() { -
CLICD v1.1.0
+CLICD v1.1.1
) diff --git a/install.sh b/install.sh index 8bd3203..5c2b015 100644 --- a/install.sh +++ b/install.sh @@ -5,17 +5,36 @@ 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}" echo "=====================================" -echo " CLICD Installer" +echo " 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() { echo "[clicd] $*" + write_log_file "[clicd] $*" +} + +warn() { + echo "[clicd][警告] $*" >&2 + write_log_file "[警告] $*" } die() { - echo "ERROR: $*" >&2 + echo "[clicd][错误] $*" >&2 + write_log_file "[错误] $*" + echo "" >&2 + echo "安装/卸载未完成。请查看日志:$LOG_FILE" >&2 + echo "如果你确认这是程序问题,请提交 issue:$ISSUE_URL" >&2 exit 1 } @@ -31,13 +50,70 @@ 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][错误] 步骤失败:$step_name,退出码:$rc" >&2 + echo "[clicd][错误] 最近 80 行日志:$LOG_FILE" >&2 + tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true + echo "" >&2 + echo "请将上述日志和系统信息提交到:$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 "Please run as root: sudo ./install.sh" - echo "Or: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh" - echo "Uninstall: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall" + echo "请使用 root 权限运行:sudo ./install.sh" + echo "或执行:curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh" + echo "卸载:curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall" + echo "问题反馈:$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 @@ -48,17 +124,21 @@ fi usage() { cat << EOF -Usage: - ./install.sh - ./install.sh uninstall +用法: + ./install.sh 安装或升级 CLICD + ./install.sh uninstall 卸载 CLICD(会删除容器、虚拟机、镜像缓存和配置数据) -Environment: - CLICD_REPO=owner/repo - CLICD_VERSION=latest|v1.0.0 +环境变量: + CLICD_REPO=owner/repo 默认:${REPO} + CLICD_VERSION=latest|v1.0.0 默认:latest + CLICD_LOG_FILE=/path/file.log 默认:${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 + +日志:${LOG_FILE} +问题反馈:${ISSUE_URL} EOF } @@ -68,7 +148,7 @@ remove_path() { return fi rm -rf "$path" - log "Removed $path" + log "已删除 $path" } unmount_path_tree() { @@ -125,17 +205,17 @@ remove_lxc_container_dir() { detach_container_loop_devices "$container_dir" if rm -rf "$container_dir" >/dev/null 2>&1; then - log "Removed $container_dir" + log "已删除 $container_dir" return fi - log "Retrying removal after terminating processes using $container_dir..." + 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 "Removed $container_dir" + log "已删除 $container_dir" } remove_kvm_domain() { @@ -158,7 +238,7 @@ remove_kvm_domain() { return fi - log "Removing KVM domain $domain..." + 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 || @@ -171,7 +251,7 @@ destroy_clicd_kvm_domains() { return fi - log "Destroying CLICD KVM domains..." + log "正在销毁 CLICD 创建的 KVM 虚拟机..." virsh list --all --name 2>/dev/null | while IFS= read -r domain; do [ -n "$domain" ] || continue remove_kvm_domain "$domain" @@ -231,7 +311,7 @@ delete_ip6tables_bridge_rules() { } cleanup_clicd_networking() { - log "Cleaning CLICD firewall and bridge rules..." + 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 @@ -269,7 +349,7 @@ remove_clicd_quota_records() { grep -v 'clicd-' "$file" > "$tmp" || true cat "$tmp" > "$file" rm -f "$tmp" - log "Cleaned CLICD quota records from $file" + log "已清理 $file 中的 CLICD 配额记录" done } @@ -277,7 +357,7 @@ remove_clicd_tmp_files() { for path in /tmp/clicd-* /tmp/clicd.*; do [ -e "$path" ] || [ -L "$path" ] || continue rm -rf "$path" - log "Removed $path" + log "已删除 $path" done } @@ -289,8 +369,28 @@ remove_clicd_swapfile() { 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][警告] 卸载会停止并删除 CLICD 服务、配置数据库、CLICD 创建的 LXC/KVM 实例和缓存数据。" >&2 + echo "[clicd][警告] 为避免误删生产数据,脚本只会删除名称形如 ct-数字 的 LXC 容器和 vm-数字 的 KVM 域。" >&2 + echo "如需确认卸载,请输入:YES" >&2 + if [ -t 0 ]; then + read answer + else + answer="" + fi + if [ "$answer" != "YES" ]; then + die "已取消卸载。如需非交互卸载,请设置 CLICD_UNINSTALL_CONFIRM=1。" + fi +} + uninstall_clicd() { - log "Uninstalling CLICD..." + confirm_uninstall + log "正在卸载 CLICD..." if has_cmd systemctl; then systemctl stop clicd >/dev/null 2>&1 || true @@ -304,8 +404,8 @@ uninstall_clicd() { rc-update del clicd default >/dev/null 2>&1 || true fi - log "Destroying LXC containers under /var/lib/lxc..." - for container_dir in /var/lib/lxc/*; do + 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 @@ -321,13 +421,12 @@ uninstall_clicd() { remove_path /var/log/clicd.log remove_path /var/log/clicd.err remove_path /root/.clicd - unmount_path_tree /var/lib/lxc - remove_path /var/lib/lxc + # /var/lib/lxc 可能包含非 CLICD 容器,生产环境不整体删除。 unmount_path_tree /var/lib/clicd remove_path /var/lib/clicd - remove_path /var/cache/lxc + # /var/cache/lxc 是 LXC 全局镜像缓存,可能被其他工具复用,生产环境不整体删除。 remove_path /var/cache/clicd - remove_path /root/clicd-backups + warn "保留 /root/clicd-backups,避免误删部署/回滚备份。确认不需要后可手动删除。" remove_clicd_tmp_files remove_clicd_swapfile @@ -341,11 +440,13 @@ uninstall_clicd() { echo "" echo "=====================================" - echo " CLICD Uninstalled" + echo " CLICD 卸载完成" echo "=====================================" - echo " Removed service, binary, SQLite/config data, LXC containers," - echo " CLICD KVM domains, VM images, image caches, firewall rules," - echo " host hooks, quota records, temp files, backups, and swapfile." + echo " 已删除服务、二进制、SQLite/配置数据、CLICD LXC/KVM 实例、" + echo " CLICD 镜像缓存、防火墙规则、主机钩子、配额记录和临时文件。" + echo " 已保留 /root/clicd-backups 和 LXC 全局缓存,避免误删生产备份/共享镜像。" + echo " 日志:$LOG_FILE" + echo " 问题反馈:$ISSUE_URL" echo "=====================================" } @@ -361,12 +462,12 @@ case "$ACTION" in exit 0 ;; *) - die "Unknown action: $ACTION" + die "未知操作:$ACTION" ;; esac install_apk() { - log "Installing dependencies with apk..." + log "正在使用 apk 安装依赖..." apk update apk add --no-cache \ ca-certificates \ @@ -393,12 +494,12 @@ install_apk() { libvirt-qemu for pkg in lxcfs shadow conntrack-tools quota-tools e2fsprogs xfsprogs cloud-utils genisoimage xorriso; do - apk add --no-cache "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg" + apk add --no-cache "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg" done } install_apt() { - log "Installing dependencies with apt..." + log "正在使用 apt 安装依赖..." export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y \ @@ -445,7 +546,7 @@ enable_el_repos() { } install_dnf() { - log "Installing dependencies with dnf..." + log "正在使用 dnf 安装依赖..." enable_el_repos dnf install -y \ ca-certificates \ @@ -475,12 +576,12 @@ install_dnf() { genisoimage for pkg in lxcfs xorriso edk2-ovmf; do - dnf install -y "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg" + dnf install -y "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg" done } install_yum() { - log "Installing dependencies with yum..." + log "正在使用 yum 安装依赖..." enable_el_repos yum install -y \ ca-certificates \ @@ -510,7 +611,7 @@ install_yum() { genisoimage for pkg in lxcfs xorriso edk2-ovmf; do - yum install -y "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg" + yum install -y "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg" done } @@ -528,7 +629,7 @@ install_dependencies() { elif has_cmd yum; then install_yum else - die "dnf/yum not found on $OS_ID" + die "当前系统 $OS_ID 未找到 dnf/yum,无法安装依赖。" fi ;; *) @@ -541,27 +642,27 @@ install_dependencies() { elif has_cmd yum; then install_yum else - die "Unsupported Linux distribution: ${OS_ID} ${OS_LIKE}" + die "暂不支持当前 Linux 发行版:${OS_ID} ${OS_LIKE}。请提交 issue 并附上 /etc/os-release。" fi ;; esac - has_cmd lxc-create || die "lxc-create is still missing after dependency installation." - has_cmd iptables || die "iptables is still missing after dependency installation." - has_cmd ip || die "iproute2/ip command is still missing after dependency installation." - has_cmd virsh || die "virsh is still missing after dependency installation." - has_cmd qemu-img || die "qemu-img is still missing after dependency installation." - has_cmd cloud-localds || die "cloud-localds is still missing after dependency installation." + 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 "one of genisoimage, mkisofs, or xorriso is required for Windows KVM setup." + die "Windows KVM 初始化需要 genisoimage、mkisofs 或 xorriso 中任意一个。" fi if [ ! -e /dev/kvm ]; then - log "Warning: /dev/kvm was not found. KVM VMs require hardware virtualization or nested virtualization." + warn "未检测到 /dev/kvm。LXC 可用,但 KVM 虚拟机需要硬件虚拟化或嵌套虚拟化。" fi } configure_kernel_networking() { - log "Enabling kernel forwarding settings..." + log "正在启用内核转发配置..." cat > /etc/sysctl.d/99-clicd.conf << 'EOF' net.ipv4.ip_forward = 1 net.ipv6.conf.all.forwarding = 1 @@ -574,7 +675,7 @@ EOF } setup_runtime_services() { - log "Configuring LXC and KVM services..." + log "正在配置 LXC 和 KVM 服务..." if is_systemd; then systemctl enable --now lxcfs >/dev/null 2>&1 || true @@ -603,11 +704,49 @@ setup_runtime_services() { return fi - die "No supported service manager found. CLICD supports systemd or OpenRC." + 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' +