Files
CLICD/install.sh
T
2026-06-07 21:08:51 +08:00

780 lines
21 KiB
Bash

#!/bin/sh
set -eu
REPO="${CLICD_REPO:-MengMengCode/CLICD}"
CLICD_INSTALL_VERSION="${CLICD_VERSION:-latest}"
ASSET="clicd-linux-amd64.tar.gz"
ACTION="${1:-install}"
echo "====================================="
echo " CLICD Installer"
echo "====================================="
log() {
echo "[clicd] $*"
}
die() {
echo "ERROR: $*" >&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
}
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"
exit 1
fi
OS_ID="unknown"
OS_LIKE=""
if [ -r /etc/os-release ]; then
. /etc/os-release
OS_ID="${ID:-unknown}"
OS_LIKE="${ID_LIKE:-}"
fi
usage() {
cat << EOF
Usage:
./install.sh
./install.sh uninstall
Environment:
CLICD_REPO=owner/repo
CLICD_VERSION=latest|v1.0.0
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
EOF
}
remove_path() {
path="$1"
if [ ! -e "$path" ] && [ ! -L "$path" ]; then
return
fi
rm -rf "$path"
log "Removed $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 "Removed $container_dir"
return
fi
log "Retrying removal after terminating processes using $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"
}
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 "Removing KVM domain $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 "Destroying CLICD KVM domains..."
virsh list --all --name 2>/dev/null | while IFS= read -r domain; do
[ -n "$domain" ] || continue
remove_kvm_domain "$domain"
done
}
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_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 "Cleaning CLICD firewall and bridge rules..."
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
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 "Cleaned CLICD quota records from $file"
done
}
remove_clicd_tmp_files() {
for path in /tmp/clicd-* /tmp/clicd.*; do
[ -e "$path" ] || [ -L "$path" ] || continue
rm -rf "$path"
log "Removed $path"
done
}
remove_clicd_swapfile() {
if [ ! -e /swapfile ]; then
return
fi
swapoff /swapfile >/dev/null 2>&1 || true
remove_path /swapfile
}
uninstall_clicd() {
log "Uninstalling 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 "Destroying LXC containers under /var/lib/lxc..."
for container_dir in /var/lib/lxc/*; do
[ -d "$container_dir" ] || continue
remove_lxc_container_dir "$container_dir"
done
destroy_clicd_kvm_domains
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
unmount_path_tree /var/lib/lxc
remove_path /var/lib/lxc
unmount_path_tree /var/lib/clicd
remove_path /var/lib/clicd
remove_path /var/cache/lxc
remove_path /var/cache/clicd
remove_path /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 " CLICD Uninstalled"
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 "====================================="
}
case "$ACTION" in
install|"")
;;
uninstall|remove)
uninstall_clicd
exit 0
;;
-h|--help|help)
usage
exit 0
;;
*)
die "Unknown action: $ACTION"
;;
esac
install_apk() {
log "Installing dependencies with 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; do
apk add --no-cache "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg"
done
}
install_apt() {
log "Installing dependencies with 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-utils \
libvirt-daemon-system \
libvirt-clients \
cloud-image-utils \
genisoimage \
xorriso \
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 "Installing dependencies with 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; do
dnf install -y "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg"
done
}
install_yum() {
log "Installing dependencies with 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; do
yum install -y "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $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 "dnf/yum not found on $OS_ID"
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 "Unsupported Linux distribution: ${OS_ID} ${OS_LIKE}"
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."
if ! has_cmd genisoimage && ! has_cmd mkisofs && ! has_cmd xorriso; then
die "one of genisoimage, mkisofs, or xorriso is required for Windows KVM setup."
fi
if [ ! -e /dev/kvm ]; then
log "Warning: /dev/kvm was not found. KVM VMs require hardware virtualization or nested virtualization."
fi
}
configure_kernel_networking() {
log "Enabling kernel forwarding settings..."
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
}
setup_runtime_services() {
log "Configuring LXC and KVM services..."
if is_systemd; then
systemctl enable --now lxcfs >/dev/null 2>&1 || true
systemctl enable --now lxc-net >/dev/null 2>&1 || true
systemctl enable --now lxc >/dev/null 2>&1 || true
systemctl enable --now libvirtd >/dev/null 2>&1 || true
systemctl enable --now virtqemud >/dev/null 2>&1 || true
systemctl enable --now virtqemud.socket >/dev/null 2>&1 || true
systemctl enable --now virtlogd.socket >/dev/null 2>&1 || true
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 "No supported service manager found. CLICD supports systemd or OpenRC."
}
setup_subids() {
log "Setting up subordinate UID/GID ranges..."
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
}
try_enable_project_quota() {
root_src="$(findmnt -no SOURCE / 2>/dev/null || true)"
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || true)"
if [ "$root_fs" != "ext4" ] || [ -z "$root_src" ] || [ ! -b "$root_src" ]; then
log "Project quota auto-enable skipped for root filesystem: ${root_fs:-unknown}"
return
fi
if ! has_cmd tune2fs; then
log "Project quota auto-enable skipped because tune2fs is unavailable."
return
fi
if tune2fs -l "$root_src" 2>/dev/null | grep -q 'project'; then
log "Ext4 project quota support already appears to be enabled."
return
fi
log "Ext4 project quota is not enabled. Disk limits will fall back to loopback images."
}
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 binary not found in current directory."
log "Downloading release package: ${download_url}"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' 0
if has_cmd curl; then
curl -fL "$download_url" -o "$tmp_dir/$ASSET"
elif has_cmd wget; then
wget -O "$tmp_dir/$ASSET" "$download_url"
else
die "curl or wget is required to download the release package."
fi
tar -xzf "$tmp_dir/$ASSET" -C "$tmp_dir"
cd "$tmp_dir/clicd-linux-amd64"
[ -f "./clicd" ] || die "Downloaded release package did not contain 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
tmp_bin="/usr/local/bin/clicd.new.$$"
cp ./clicd "$tmp_bin"
chmod +x "$tmp_bin"
mv -f "$tmp_bin" /usr/local/bin/clicd
chmod +x /usr/local/bin/clicd
log "Installed binary: /usr/local/bin/clicd"
}
install_systemd_service() {
cat > /etc/systemd/system/clicd.service << 'EOF'
[Unit]
Description=CLICD - LXC/KVM Container Manager
After=network.target lxc.service libvirtd.service virtqemud.service
Wants=libvirtd.service
[Service]
Type=simple
ExecStart=/usr/local/bin/clicd server
Restart=always
RestartSec=5
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 "Installing CLICD service..."
if is_systemd; then
install_systemd_service
elif is_openrc; then
install_openrc_service
else
die "No supported service manager found. CLICD supports systemd or OpenRC."
fi
}
print_summary() {
echo ""
echo "====================================="
echo " Installation Complete"
echo "====================================="
echo " Web: http://YOUR_SERVER_IP:8999"
echo " Binary: /usr/local/bin/clicd"
if is_systemd; then
echo " Service: systemctl {start|stop|restart|status} clicd"
echo " Logs: journalctl -u clicd -f"
elif is_openrc; then
echo " Service: rc-service clicd {start|stop|restart|status}"
echo " Logs: tail -f /var/log/clicd.log /var/log/clicd.err"
fi
echo "====================================="
echo ""
echo "Initial credentials, if this was the first run:"
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 "If no password is shown, this server already had /root/.clicd/config.db."
echo "The existing admin password cannot be recovered from the bcrypt hash."
}
install_dependencies
configure_kernel_networking
setup_runtime_services
setup_subids
try_enable_project_quota
download_release_if_needed
install_binary
install_service
sleep 2
print_summary