mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db347aeeb6 | |||
| 3c9ee3552e | |||
| 48dee79129 |
@@ -37,7 +37,13 @@ CLICD 是一个面向 LXC 的轻量容器管理面板,提供 Web 控制台、C
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
推荐使用 GitHub Actions 构建出的 Release 产物。下载 `clicd-linux-amd64.tar.gz` 后在目标服务器上执行:
|
推荐使用最新 Release 一键安装。在目标服务器上执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以下载 GitHub Actions 构建出的 Release 产物 `clicd-linux-amd64.tar.gz` 后手动安装:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar -xzf clicd-linux-amd64.tar.gz
|
tar -xzf clicd-linux-amd64.tar.gz
|
||||||
@@ -66,64 +72,18 @@ Password: 随机 16 位密码
|
|||||||
journalctl -u clicd --no-pager -n 80 | grep -E "Username:|Password:"
|
journalctl -u clicd --no-pager -n 80 | grep -E "Username:|Password:"
|
||||||
```
|
```
|
||||||
|
|
||||||
## GitHub Actions 构建
|
卸载 CLICD:
|
||||||
|
|
||||||
仓库内置 `.github/workflows/build.yml`:
|
|
||||||
|
|
||||||
- 推送到 `main` 或 `master` 时自动构建 Linux amd64 产物。
|
|
||||||
- 创建 `v*` 标签时自动发布 GitHub Release。
|
|
||||||
- 支持手动 `workflow_dispatch` 构建。
|
|
||||||
|
|
||||||
发布版本示例:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git tag v1.0.0
|
curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh -s -- uninstall
|
||||||
git push origin v1.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Release 会包含:
|
默认只删除 CLICD 服务和 `/usr/local/bin/clicd`,保留 `/root/.clicd` 配置数据和 `/var/lib/lxc` 容器。需要同时删除配置数据时:
|
||||||
|
|
||||||
```text
|
|
||||||
clicd-linux-amd64.tar.gz
|
|
||||||
clicd-linux-amd64
|
|
||||||
SHA256SUMS
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLI 模式
|
|
||||||
|
|
||||||
进入 CLI:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clicd cli
|
curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh -s -- uninstall --purge-data
|
||||||
```
|
```
|
||||||
|
|
||||||
仅使用 CLI,不自动拉起 Web 服务:
|

|
||||||
|

|
||||||
```bash
|

|
||||||
systemctl stop clicd
|
|
||||||
systemctl disable clicd
|
|
||||||
clicd cli --no-web
|
|
||||||
```
|
|
||||||
|
|
||||||
重新启用 Web 控制台:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl enable --now clicd
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常用服务命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl status clicd
|
|
||||||
systemctl restart clicd
|
|
||||||
journalctl -u clicd -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 需要 root 权限安装和运行。
|
|
||||||
- 宿主机需要支持 LXC。
|
|
||||||
- NAT 和端口映射依赖 iptables。
|
|
||||||
- 安全告警依赖 conntrack 或 `/proc/net/nf_conntrack`。
|
|
||||||
- IPv6 分配要求宿主机拥有可用公网 IPv6 地址段。
|
|
||||||
- 配置文件位于 `/root/.clicd/config.json`,其中包含敏感信息,不要提交到公开仓库。
|
|
||||||
|
|||||||
+144
-10
@@ -21,7 +21,7 @@ func Run() {
|
|||||||
for {
|
for {
|
||||||
clearScreen()
|
clearScreen()
|
||||||
printMenu()
|
printMenu()
|
||||||
fmt.Print("\nSelect action [1-9,0/q]: ")
|
fmt.Print("\nSelect action [1-10,0/q]: ")
|
||||||
input, _ := reader.ReadString('\n')
|
input, _ := reader.ReadString('\n')
|
||||||
input = strings.TrimSpace(input)
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
@@ -62,6 +62,10 @@ func Run() {
|
|||||||
clearScreen()
|
clearScreen()
|
||||||
cliToggleWebPanel()
|
cliToggleWebPanel()
|
||||||
waitEnter(reader)
|
waitEnter(reader)
|
||||||
|
case "10":
|
||||||
|
clearScreen()
|
||||||
|
cliUninstall(reader)
|
||||||
|
return
|
||||||
case "0":
|
case "0":
|
||||||
clearScreen()
|
clearScreen()
|
||||||
cliShowInfo()
|
cliShowInfo()
|
||||||
@@ -101,6 +105,7 @@ func printMenu() {
|
|||||||
fmt.Println(" 7. Reinstall container")
|
fmt.Println(" 7. Reinstall container")
|
||||||
fmt.Println(" 8. Reset web admin password")
|
fmt.Println(" 8. Reset web admin password")
|
||||||
fmt.Printf(" 9. %s web panel\n", webStatus)
|
fmt.Printf(" 9. %s web panel\n", webStatus)
|
||||||
|
fmt.Println(" 10. Uninstall CLICD")
|
||||||
fmt.Println(" 0. System info")
|
fmt.Println(" 0. System info")
|
||||||
fmt.Println(" q. Quit")
|
fmt.Println(" q. Quit")
|
||||||
}
|
}
|
||||||
@@ -281,8 +286,7 @@ func cliResetPassword(reader *bufio.Reader) {
|
|||||||
|
|
||||||
func cliToggleWebPanel() {
|
func cliToggleWebPanel() {
|
||||||
if isWebPanelRunning() {
|
if isWebPanelRunning() {
|
||||||
cmd := exec.Command("systemctl", "stop", "clicd")
|
if err := stopService("clicd"); err != nil {
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf("Failed to stop web panel: %v\n", err)
|
fmt.Printf("Failed to stop web panel: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -290,8 +294,7 @@ func cliToggleWebPanel() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("systemctl", "start", "clicd")
|
if err := startService("clicd"); err != nil {
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf("Failed to start web panel: %v\n", err)
|
fmt.Printf("Failed to start web panel: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -299,12 +302,143 @@ func cliToggleWebPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isWebPanelRunning() bool {
|
func isWebPanelRunning() bool {
|
||||||
cmd := exec.Command("systemctl", "is-active", "clicd")
|
if commandExists("systemctl") {
|
||||||
output, err := cmd.Output()
|
cmd := exec.Command("systemctl", "is-active", "clicd")
|
||||||
if err != nil {
|
output, err := cmd.Output()
|
||||||
return false
|
if err == nil && strings.TrimSpace(string(output)) == "active" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(output)) == "active"
|
if commandExists("rc-service") {
|
||||||
|
cmd := exec.Command("rc-service", "clicd", "status")
|
||||||
|
return cmd.Run() == nil
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func cliUninstall(reader *bufio.Reader) {
|
||||||
|
fmt.Println("\n--- Uninstall CLICD ---")
|
||||||
|
fmt.Println("This removes the CLICD service and /usr/local/bin/clicd.")
|
||||||
|
fmt.Println("LXC containers and /root/.clicd are kept unless you explicitly choose to delete them.")
|
||||||
|
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Println("Uninstall must be run as root.")
|
||||||
|
fmt.Println("Run: sudo clicd cli --no-web")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm := promptString(reader, "Type uninstall to continue", "no")
|
||||||
|
if strings.ToLower(confirm) != "uninstall" {
|
||||||
|
fmt.Println("Canceled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removeData := strings.ToLower(promptString(reader, "Delete /root/.clicd config/data? Type delete-data", "no")) == "delete-data"
|
||||||
|
removeContainers := strings.ToLower(promptString(reader, "Destroy CLICD-managed LXC containers? Type delete-containers", "no")) == "delete-containers"
|
||||||
|
|
||||||
|
if removeContainers {
|
||||||
|
destroyManagedContainers()
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAndRemoveService()
|
||||||
|
removePath("/usr/local/bin/clicd")
|
||||||
|
removePath("/etc/sysctl.d/99-clicd.conf")
|
||||||
|
removePath("/var/log/clicd.log")
|
||||||
|
removePath("/var/log/clicd.err")
|
||||||
|
|
||||||
|
if removeData {
|
||||||
|
removePath("/root/.clicd")
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadSysctl()
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("CLICD has been uninstalled.")
|
||||||
|
if !removeData {
|
||||||
|
fmt.Println("Kept data: /root/.clicd")
|
||||||
|
}
|
||||||
|
if !removeContainers {
|
||||||
|
fmt.Println("Kept LXC containers under /var/lib/lxc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func destroyManagedContainers() {
|
||||||
|
containers := append([]config.Container(nil), config.AppConfig.Containers...)
|
||||||
|
if len(containers) == 0 {
|
||||||
|
fmt.Println("No CLICD-managed containers found in config.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range containers {
|
||||||
|
fmt.Printf("Destroying container %s...\n", c.Name)
|
||||||
|
if err := manager.DestroyContainer(c.ID); err != nil {
|
||||||
|
fmt.Printf("Failed to destroy %s: %v\n", c.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAndRemoveService() {
|
||||||
|
if commandExists("systemctl") {
|
||||||
|
runQuiet("systemctl", "stop", "clicd")
|
||||||
|
runQuiet("systemctl", "disable", "clicd")
|
||||||
|
removePath("/etc/systemd/system/clicd.service")
|
||||||
|
runQuiet("systemctl", "daemon-reload")
|
||||||
|
runQuiet("systemctl", "reset-failed", "clicd")
|
||||||
|
}
|
||||||
|
|
||||||
|
if commandExists("rc-service") {
|
||||||
|
runQuiet("rc-service", "clicd", "stop")
|
||||||
|
}
|
||||||
|
if commandExists("rc-update") {
|
||||||
|
runQuiet("rc-update", "del", "clicd", "default")
|
||||||
|
}
|
||||||
|
removePath("/etc/init.d/clicd")
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePath(path string) {
|
||||||
|
if _, err := os.Lstat(path); os.IsNotExist(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(path); err != nil {
|
||||||
|
fmt.Printf("Failed to remove %s: %v\n", path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Removed %s\n", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadSysctl() {
|
||||||
|
if commandExists("sysctl") {
|
||||||
|
runQuiet("sysctl", "--system")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandExists(name string) bool {
|
||||||
|
_, err := exec.LookPath(name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQuiet(name string, args ...string) {
|
||||||
|
_ = exec.Command(name, args...).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopService(name string) error {
|
||||||
|
if commandExists("systemctl") {
|
||||||
|
return exec.Command("systemctl", "stop", name).Run()
|
||||||
|
}
|
||||||
|
if commandExists("rc-service") {
|
||||||
|
return exec.Command("rc-service", name, "stop").Run()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no supported service manager found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func startService(name string) error {
|
||||||
|
if commandExists("systemctl") {
|
||||||
|
return exec.Command("systemctl", "start", name).Run()
|
||||||
|
}
|
||||||
|
if commandExists("rc-service") {
|
||||||
|
return exec.Command("rc-service", name, "start").Run()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no supported service manager found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func cliShowInfo() {
|
func cliShowInfo() {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
+486
-105
@@ -1,105 +1,428 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
set -euo pipefail
|
set -eu
|
||||||
|
|
||||||
|
REPO="${CLICD_REPO:-MengMengCode/CLICD}"
|
||||||
|
CLICD_INSTALL_VERSION="${CLICD_VERSION:-latest}"
|
||||||
|
ASSET="clicd-linux-amd64.tar.gz"
|
||||||
|
ACTION="${1:-install}"
|
||||||
|
|
||||||
echo "====================================="
|
echo "====================================="
|
||||||
echo " CLICD Installation"
|
echo " CLICD Installer"
|
||||||
echo "====================================="
|
echo "====================================="
|
||||||
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
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 "Please run as root: sudo ./install.sh"
|
||||||
echo "Or: curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo bash"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "./clicd" ]; then
|
OS_ID="unknown"
|
||||||
REPO="${CLICD_REPO:-MengMengCode/CLICD}"
|
OS_LIKE=""
|
||||||
VERSION="${CLICD_VERSION:-latest}"
|
if [ -r /etc/os-release ]; then
|
||||||
ASSET="clicd-linux-amd64.tar.gz"
|
. /etc/os-release
|
||||||
|
OS_ID="${ID:-unknown}"
|
||||||
if [ "$VERSION" = "latest" ]; then
|
OS_LIKE="${ID_LIKE:-}"
|
||||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/latest/download/${ASSET}"
|
|
||||||
else
|
|
||||||
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "clicd binary not found in current directory."
|
|
||||||
echo "Downloading release package: ${DOWNLOAD_URL}"
|
|
||||||
|
|
||||||
TMP_DIR="$(mktemp -d)"
|
|
||||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
curl -fL "$DOWNLOAD_URL" -o "$TMP_DIR/$ASSET"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
wget -O "$TMP_DIR/$ASSET" "$DOWNLOAD_URL"
|
|
||||||
else
|
|
||||||
echo "ERROR: curl or wget is required to download the release package."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
tar -xzf "$TMP_DIR/$ASSET" -C "$TMP_DIR"
|
|
||||||
cd "$TMP_DIR/clicd-linux-amd64"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v lxc-create >/dev/null 2>&1; then
|
usage() {
|
||||||
echo "LXC is not installed. Installing dependencies..."
|
cat << EOF
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
Usage:
|
||||||
apt-get update
|
./install.sh
|
||||||
apt-get install -y lxc lxc-templates bridge-utils xz-utils quota
|
./install.sh uninstall [--purge-data] [--delete-containers]
|
||||||
elif command -v yum >/dev/null 2>&1; then
|
|
||||||
yum install -y epel-release
|
Environment:
|
||||||
yum install -y lxc lxc-templates xz quota
|
CLICD_REPO=owner/repo
|
||||||
elif command -v dnf >/dev/null 2>&1; then
|
CLICD_VERSION=latest|v1.0.0
|
||||||
dnf install -y lxc lxc-templates xz quota
|
|
||||||
else
|
Examples:
|
||||||
echo "Could not detect package manager. Please install LXC manually."
|
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh
|
||||||
exit 1
|
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
|
fi
|
||||||
fi
|
rm -rf "$path"
|
||||||
|
log "Removed $path"
|
||||||
|
}
|
||||||
|
|
||||||
# Setup subordinate UID/GID for unprivileged containers
|
uninstall_clicd() {
|
||||||
echo "Setting up subordinate UID/GID ranges..."
|
purge_data=0
|
||||||
grep -q '^root:' /etc/subuid 2>/dev/null || echo 'root:100000:65536' >> /etc/subuid
|
delete_containers=0
|
||||||
grep -q '^root:' /etc/subgid 2>/dev/null || echo 'root:100000:65536' >> /etc/subgid
|
|
||||||
|
|
||||||
# Enable ext4 project quota if supported
|
shift || true
|
||||||
if tune2fs -l /dev/sda1 2>/dev/null | grep -q 'Filesystem features'; then
|
while [ "$#" -gt 0 ]; do
|
||||||
echo "Enabling ext4 project quota..."
|
case "$1" in
|
||||||
mkdir -p /etc/initramfs-tools/hooks /etc/initramfs-tools/scripts/local-premount
|
--purge-data)
|
||||||
|
purge_data=1
|
||||||
# Hook to copy tune2fs into initramfs
|
;;
|
||||||
cat > /etc/initramfs-tools/hooks/tune2fs-hook << 'HOOK'
|
--delete-containers)
|
||||||
#!/bin/sh
|
delete_containers=1
|
||||||
PREREQ=""
|
;;
|
||||||
prereqs() { echo "$PREREQ"; }
|
-h|--help)
|
||||||
case "$1" in prereqs) prereqs; exit 0;; esac
|
usage
|
||||||
. /usr/share/initramfs-tools/hook-functions
|
exit 0
|
||||||
copy_exec /sbin/tune2fs /sbin/tune2fs
|
;;
|
||||||
copy_exec /usr/sbin/setquota /usr/sbin/setquota
|
*)
|
||||||
HOOK
|
die "Unknown uninstall option: $1"
|
||||||
chmod +x /etc/initramfs-tools/hooks/tune2fs-hook
|
;;
|
||||||
|
esac
|
||||||
# Script to run tune2fs before mount
|
shift
|
||||||
cat > /etc/initramfs-tools/scripts/local-premount/prjquota << 'SCRIPT'
|
done
|
||||||
#!/bin/sh
|
|
||||||
PREREQ=""
|
|
||||||
prereqs() { echo "$PREREQ"; }
|
|
||||||
case "$1" in prereqs) prereqs; exit 0;; esac
|
|
||||||
/sbin/tune2fs -O project -Q prjquota /dev/sda1 2>/dev/null
|
|
||||||
SCRIPT
|
|
||||||
chmod +x /etc/initramfs-tools/scripts/local-premount/prjquota
|
|
||||||
|
|
||||||
update-initramfs -u -k all 2>/dev/null || true
|
|
||||||
|
|
||||||
# Add prjquota to fstab if not already there
|
|
||||||
grep -q 'prjquota' /etc/fstab 2>/dev/null || sed -i 's|ext4 rw,|ext4 rw,prjquota,|' /etc/fstab
|
|
||||||
fi
|
|
||||||
|
|
||||||
cp ./clicd /usr/local/bin/clicd
|
log "Uninstalling CLICD..."
|
||||||
chmod +x /usr/local/bin/clicd
|
|
||||||
echo "Installed binary: /usr/local/bin/clicd"
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/clicd.service << 'EOF'
|
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
|
||||||
|
|
||||||
|
if [ "$delete_containers" -eq 1 ]; then
|
||||||
|
log "Destroying CLICD-style LXC containers named ct-*..."
|
||||||
|
for container_dir in /var/lib/lxc/ct-*; do
|
||||||
|
[ -d "$container_dir" ] || continue
|
||||||
|
container_name="$(basename "$container_dir")"
|
||||||
|
lxc-stop -n "$container_name" -k >/dev/null 2>&1 || true
|
||||||
|
lxc-destroy -n "$container_name" -f >/dev/null 2>&1 || true
|
||||||
|
remove_path "$container_dir"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if [ "$purge_data" -eq 1 ]; then
|
||||||
|
remove_path /root/.clicd
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 "====================================="
|
||||||
|
if [ "$purge_data" -eq 0 ]; then
|
||||||
|
echo " Kept data: /root/.clicd"
|
||||||
|
fi
|
||||||
|
if [ "$delete_containers" -eq 0 ]; then
|
||||||
|
echo " Kept LXC containers: /var/lib/lxc"
|
||||||
|
echo " To delete CLICD-style ct-* containers too:"
|
||||||
|
echo " ./install.sh uninstall --delete-containers"
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
|
||||||
|
for pkg in lxcfs shadow conntrack-tools quota-tools e2fsprogs xfsprogs; 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
dnf install -y lxcfs >/dev/null 2>&1 || log "Optional package not installed: lxcfs"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
yum install -y lxcfs >/dev/null 2>&1 || log "Optional package not installed: lxcfs"
|
||||||
|
}
|
||||||
|
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
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_lxc_services() {
|
||||||
|
log "Configuring LXC 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
|
||||||
|
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
|
||||||
|
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() {
|
||||||
|
cp ./clicd /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]
|
[Unit]
|
||||||
Description=CLICD - LXC Container Manager
|
Description=CLICD - LXC Container Manager
|
||||||
After=network.target lxc.service
|
After=network.target lxc.service
|
||||||
@@ -115,23 +438,81 @@ Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable clicd
|
systemctl enable clicd
|
||||||
systemctl restart clicd
|
systemctl restart clicd
|
||||||
|
}
|
||||||
|
|
||||||
|
install_openrc_service() {
|
||||||
|
cat > /etc/init.d/clicd << 'EOF'
|
||||||
|
#!/sbin/openrc-run
|
||||||
|
|
||||||
|
name="CLICD"
|
||||||
|
description="CLICD - LXC 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
|
||||||
|
}
|
||||||
|
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.json."
|
||||||
|
echo "The existing admin password cannot be recovered from the bcrypt hash."
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dependencies
|
||||||
|
configure_kernel_networking
|
||||||
|
setup_lxc_services
|
||||||
|
setup_subids
|
||||||
|
try_enable_project_quota
|
||||||
|
download_release_if_needed
|
||||||
|
install_binary
|
||||||
|
install_service
|
||||||
sleep 2
|
sleep 2
|
||||||
|
print_summary
|
||||||
echo ""
|
|
||||||
echo "====================================="
|
|
||||||
echo " Installation Complete"
|
|
||||||
echo "====================================="
|
|
||||||
echo " Web: http://YOUR_SERVER_IP:8999"
|
|
||||||
echo " Service: systemctl {start|stop|restart|status} clicd"
|
|
||||||
echo " Logs: journalctl -u clicd -f"
|
|
||||||
echo "====================================="
|
|
||||||
echo ""
|
|
||||||
echo "Initial credentials, if this was the first run:"
|
|
||||||
journalctl -u clicd --no-pager -n 80 | grep -E "Username:|Password:" || true
|
|
||||||
echo ""
|
|
||||||
echo "If no password is shown, this server already had /root/.clicd/config.json."
|
|
||||||
echo "The existing admin password cannot be recovered from the bcrypt hash."
|
|
||||||
|
|||||||
Reference in New Issue
Block a user