Compare commits

...

3 Commits

Author SHA1 Message Date
MengMengCode bb8a646de7 Make uninstall remove all CLICD data 2026-06-05 20:12:36 +08:00
MengMengCode a2da61e076 Install binary with atomic replacement 2026-06-05 20:03:16 +08:00
MengMengCode 49a6981e28 Import existing LXC containers into CLICD 2026-06-05 20:01:24 +08:00
5 changed files with 77 additions and 124 deletions
+2 -37
View File
@@ -37,53 +37,18 @@ CLICD 是一个面向 LXC 的轻量容器管理面板,提供 Web 控制台、C
## 安装 ## 安装
推荐使用最新 Release 一键安装。在目标服务器上执行 一键安装
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh
``` ```
也可以下载 GitHub Actions 构建出的 Release 产物 `clicd-linux-amd64.tar.gz` 后手动安装 一键卸载
```bash
tar -xzf clicd-linux-amd64.tar.gz
cd clicd-linux-amd64
sudo ./install.sh
```
安装完成后访问:
```text
http://YOUR_SERVER_IP:8999
```
首次启动时会自动初始化管理员账号:
```text
Username: admin
Password: 随机 16 位密码
```
安装脚本会尝试从 systemd 日志中输出初始账号密码。如果机器上已经存在 `/root/.clicd/config.json`,则不会重新生成密码。
查看初始密码日志:
```bash
journalctl -u clicd --no-pager -n 80 | grep -E "Username:|Password:"
```
卸载 CLICD
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh -s -- uninstall curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh -s -- uninstall
``` ```
默认只删除 CLICD 服务和 `/usr/local/bin/clicd`,保留 `/root/.clicd` 配置数据和 `/var/lib/lxc` 容器。需要同时删除配置数据时:
```bash
curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh | sudo sh -s -- uninstall --purge-data
```
![alt text](/img/image.png) ![alt text](/img/image.png)
![alt text](/img/image-1.png) ![alt text](/img/image-1.png)
![alt text](/img/image-2.png) ![alt text](/img/image-2.png)
+21 -30
View File
@@ -113,7 +113,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. Import existing ct-* containers") fmt.Println(" 10. Import existing LXC containers")
fmt.Println(" 11. Uninstall CLICD") fmt.Println(" 11. Uninstall CLICD")
fmt.Println(" 0. System info") fmt.Println(" 0. System info")
fmt.Println(" q. Quit") fmt.Println(" q. Quit")
@@ -330,9 +330,9 @@ func isWebPanelRunning() bool {
} }
func cliImportExistingContainers() { func cliImportExistingContainers() {
fmt.Println("\n--- Import existing CLICD containers ---") fmt.Println("\n--- Import existing LXC containers ---")
fmt.Println("This imports LXC containers named ct-{id} from /var/lib/lxc into CLICD config.") fmt.Println("This imports containers from /var/lib/lxc into CLICD config.")
fmt.Println("Containers with names like ubuntu or alpine are skipped because CLICD requires ct-{id}.") fmt.Println("Imported containers keep their real LXC name so Web and CLI manage the same container.")
imported, err := manager.ImportExistingClicdContainers() imported, err := manager.ImportExistingClicdContainers()
if err != nil { if err != nil {
@@ -354,7 +354,7 @@ func cliImportExistingContainers() {
func cliUninstall(reader *bufio.Reader) { func cliUninstall(reader *bufio.Reader) {
fmt.Println("\n--- Uninstall CLICD ---") fmt.Println("\n--- Uninstall CLICD ---")
fmt.Println("This removes the CLICD service and /usr/local/bin/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.") fmt.Println("This also deletes /root/.clicd, all LXC containers under /var/lib/lxc, and LXC image cache under /var/cache/lxc.")
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
fmt.Println("Uninstall must be run as root.") fmt.Println("Uninstall must be run as root.")
@@ -368,47 +368,38 @@ func cliUninstall(reader *bufio.Reader) {
return return
} }
removeData := strings.ToLower(promptString(reader, "Delete /root/.clicd config/data? Type delete-data", "no")) == "delete-data" destroyAllLXCContainers()
removeContainers := strings.ToLower(promptString(reader, "Destroy CLICD-managed LXC containers? Type delete-containers", "no")) == "delete-containers"
if removeContainers {
destroyManagedContainers()
}
stopAndRemoveService() stopAndRemoveService()
removePath("/usr/local/bin/clicd") removePath("/usr/local/bin/clicd")
removePath("/etc/sysctl.d/99-clicd.conf") removePath("/etc/sysctl.d/99-clicd.conf")
removePath("/var/log/clicd.log") removePath("/var/log/clicd.log")
removePath("/var/log/clicd.err") removePath("/var/log/clicd.err")
removePath("/root/.clicd")
if removeData { removePath("/var/lib/lxc")
removePath("/root/.clicd") removePath("/var/cache/lxc")
}
reloadSysctl() reloadSysctl()
fmt.Println() fmt.Println()
fmt.Println("CLICD has been uninstalled.") fmt.Println("CLICD has been uninstalled.")
if !removeData { fmt.Println("Removed service, binary, config, containers, and LXC image cache.")
fmt.Println("Kept data: /root/.clicd")
}
if !removeContainers {
fmt.Println("Kept LXC containers under /var/lib/lxc")
}
} }
func destroyManagedContainers() { func destroyAllLXCContainers() {
containers := append([]config.Container(nil), config.AppConfig.Containers...) entries, err := os.ReadDir("/var/lib/lxc")
if len(containers) == 0 { if err != nil {
fmt.Println("No CLICD-managed containers found in config.")
return return
} }
for _, c := range containers { for _, entry := range entries {
fmt.Printf("Destroying container %s...\n", c.Name) if !entry.IsDir() {
if err := manager.DestroyContainer(c.ID); err != nil { continue
fmt.Printf("Failed to destroy %s: %v\n", c.Name, err)
} }
name := entry.Name()
fmt.Printf("Destroying LXC container %s...\n", name)
runQuiet("lxc-stop", "-n", name, "-k")
runQuiet("lxc-destroy", "-n", name, "-f")
removePath("/var/lib/lxc/" + name)
} }
} }
+4
View File
@@ -68,6 +68,7 @@ type Container struct {
ID int `json:"id"` ID int `json:"id"`
UUID string `json:"uuid"` UUID string `json:"uuid"`
Name string `json:"name"` Name string `json:"name"`
LXCName string `json:"lxc_name,omitempty"`
Template string `json:"template"` Template string `json:"template"`
VCPU float64 `json:"vcpu"` VCPU float64 `json:"vcpu"`
RAMMB int `json:"ram_mb"` RAMMB int `json:"ram_mb"`
@@ -97,6 +98,9 @@ type Container struct {
// LxcName returns the internal LXC container name (ct-{id}) // LxcName returns the internal LXC container name (ct-{id})
func (c *Container) LxcName() string { func (c *Container) LxcName() string {
if c.LXCName != "" {
return c.LXCName
}
return fmt.Sprintf("ct-%d", c.ID) return fmt.Sprintf("ct-%d", c.ID)
} }
+23 -11
View File
@@ -1915,9 +1915,9 @@ func (m *Manager) ListContainers() ([]config.Container, error) {
return containers, nil return containers, nil
} }
// ImportExistingClicdContainers imports LXC containers named ct-{id} into the // ImportExistingClicdContainers imports existing LXC containers into the CLICD
// CLICD config. Containers with arbitrary LXC names cannot be imported because // config. Native CLICD containers keep ct-{id}; arbitrary LXC names are stored
// CLICD derives the runtime LXC name from the numeric container ID. // in Container.LXCName so Web and CLI can manage the same imported container.
func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) { func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
entries, err := os.ReadDir(m.LxcPath) entries, err := os.ReadDir(m.LxcPath)
if err != nil { if err != nil {
@@ -1926,10 +1926,12 @@ func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
existingIDs := make(map[int]bool) existingIDs := make(map[int]bool)
existingNames := make(map[string]bool) existingNames := make(map[string]bool)
existingLXCNames := make(map[string]bool)
maxID := config.AppConfig.NextContainerID - 1 maxID := config.AppConfig.NextContainerID - 1
for _, c := range config.AppConfig.Containers { for _, c := range config.AppConfig.Containers {
existingIDs[c.ID] = true existingIDs[c.ID] = true
existingNames[c.Name] = true existingNames[c.Name] = true
existingLXCNames[c.LxcName()] = true
if c.ID > maxID { if c.ID > maxID {
maxID = c.ID maxID = c.ID
} }
@@ -1941,22 +1943,30 @@ func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
if !entry.IsDir() { if !entry.IsDir() {
continue continue
} }
matches := re.FindStringSubmatch(entry.Name()) lxcName := entry.Name()
if len(matches) != 2 { if existingLXCNames[lxcName] {
continue continue
} }
id, err := strconv.Atoi(matches[1]) id := 0
if err != nil || id <= 0 || existingIDs[id] { if matches := re.FindStringSubmatch(lxcName); len(matches) == 2 {
continue if parsed, err := strconv.Atoi(matches[1]); err == nil && parsed > 0 && !existingIDs[parsed] {
id = parsed
}
}
if id == 0 {
id = maxID + 1
for existingIDs[id] {
id++
}
} }
name := entry.Name() name := lxcName
if existingNames[name] { if existingNames[name] {
name = fmt.Sprintf("imported-%d", id) name = fmt.Sprintf("imported-%d", id)
} }
status, err := m.GetContainerStatus(entry.Name()) status, err := m.GetContainerStatus(lxcName)
if err != nil || status == "" { if err != nil || status == "" {
status = "unknown" status = "unknown"
} }
@@ -1965,6 +1975,7 @@ func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
ID: id, ID: id,
UUID: config.NewContainerUUID(), UUID: config.NewContainerUUID(),
Name: name, Name: name,
LXCName: lxcName,
Template: "imported", Template: "imported",
VCPU: 1, VCPU: 1,
RAMMB: 512, RAMMB: 512,
@@ -1978,7 +1989,7 @@ func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
} }
if status == "running" { if status == "running" {
if ip, err := m.GetContainerIP(entry.Name()); err == nil { if ip, err := m.GetContainerIP(lxcName); err == nil {
c.IP = ip c.IP = ip
} }
} }
@@ -1987,6 +1998,7 @@ func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
imported = append(imported, c) imported = append(imported, c)
existingIDs[id] = true existingIDs[id] = true
existingNames[name] = true existingNames[name] = true
existingLXCNames[lxcName] = true
if id > maxID { if id > maxID {
maxID = id maxID = id
} }
+27 -46
View File
@@ -50,7 +50,7 @@ usage() {
cat << EOF cat << EOF
Usage: Usage:
./install.sh ./install.sh
./install.sh uninstall [--purge-data] [--delete-containers] ./install.sh uninstall
Environment: Environment:
CLICD_REPO=owner/repo CLICD_REPO=owner/repo
@@ -72,29 +72,6 @@ remove_path() {
} }
uninstall_clicd() { uninstall_clicd() {
purge_data=0
delete_containers=0
shift || true
while [ "$#" -gt 0 ]; do
case "$1" in
--purge-data)
purge_data=1
;;
--delete-containers)
delete_containers=1
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown uninstall option: $1"
;;
esac
shift
done
log "Uninstalling CLICD..." log "Uninstalling CLICD..."
if has_cmd systemctl; then if has_cmd systemctl; then
@@ -109,16 +86,18 @@ uninstall_clicd() {
rc-update del clicd default >/dev/null 2>&1 || true rc-update del clicd default >/dev/null 2>&1 || true
fi fi
if [ "$delete_containers" -eq 1 ]; then log "Destroying LXC containers under /var/lib/lxc..."
log "Destroying CLICD-style LXC containers named ct-*..." for container_dir in /var/lib/lxc/*; do
for container_dir in /var/lib/lxc/ct-*; do [ -d "$container_dir" ] || continue
[ -d "$container_dir" ] || continue container_name="$(basename "$container_dir")"
container_name="$(basename "$container_dir")" if has_cmd lxc-stop; then
lxc-stop -n "$container_name" -k >/dev/null 2>&1 || true 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 lxc-destroy -n "$container_name" -f >/dev/null 2>&1 || true
remove_path "$container_dir" fi
done remove_path "$container_dir"
fi done
remove_path /etc/systemd/system/clicd.service remove_path /etc/systemd/system/clicd.service
remove_path /etc/init.d/clicd remove_path /etc/init.d/clicd
@@ -126,10 +105,9 @@ uninstall_clicd() {
remove_path /etc/sysctl.d/99-clicd.conf remove_path /etc/sysctl.d/99-clicd.conf
remove_path /var/log/clicd.log remove_path /var/log/clicd.log
remove_path /var/log/clicd.err remove_path /var/log/clicd.err
remove_path /root/.clicd
if [ "$purge_data" -eq 1 ]; then remove_path /var/lib/lxc
remove_path /root/.clicd remove_path /var/cache/lxc
fi
if has_cmd systemctl; then if has_cmd systemctl; then
systemctl daemon-reload >/dev/null 2>&1 || true systemctl daemon-reload >/dev/null 2>&1 || true
@@ -143,14 +121,7 @@ uninstall_clicd() {
echo "=====================================" echo "====================================="
echo " CLICD Uninstalled" echo " CLICD Uninstalled"
echo "=====================================" echo "====================================="
if [ "$purge_data" -eq 0 ]; then echo " Removed service, binary, config, containers, and LXC image cache."
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 "=====================================" echo "====================================="
} }
@@ -158,7 +129,7 @@ case "$ACTION" in
install|"") install|"")
;; ;;
uninstall|remove) uninstall|remove)
uninstall_clicd "$@" uninstall_clicd
exit 0 exit 0
;; ;;
-h|--help|help) -h|--help|help)
@@ -416,7 +387,17 @@ download_release_if_needed() {
} }
install_binary() { install_binary() {
cp ./clicd /usr/local/bin/clicd 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 chmod +x /usr/local/bin/clicd
log "Installed binary: /usr/local/bin/clicd" log "Installed binary: /usr/local/bin/clicd"
} }