Compare commits

...

4 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
MengMengCode 7613d9135b Sync CLI and Web container state 2026-06-05 19:58:42 +08:00
6 changed files with 206 additions and 112 deletions
+1
View File
@@ -62,3 +62,4 @@ backend/tmp/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
linux.txt
+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)
+73 -29
View File
@@ -19,9 +19,13 @@ func Run() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
if _, err := config.InitConfig(); err != nil {
fmt.Printf("Failed to reload config: %v\n", err)
waitEnter(reader)
}
clearScreen() clearScreen()
printMenu() printMenu()
fmt.Print("\nSelect action [1-10,0/q]: ") fmt.Print("\nSelect action [1-11,0/q]: ")
input, _ := reader.ReadString('\n') input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
@@ -63,6 +67,10 @@ func Run() {
cliToggleWebPanel() cliToggleWebPanel()
waitEnter(reader) waitEnter(reader)
case "10": case "10":
clearScreen()
cliImportExistingContainers()
waitEnter(reader)
case "11":
clearScreen() clearScreen()
cliUninstall(reader) cliUninstall(reader)
return return
@@ -105,7 +113,8 @@ 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(" 10. Import existing LXC containers")
fmt.Println(" 11. Uninstall CLICD")
fmt.Println(" 0. System info") fmt.Println(" 0. System info")
fmt.Println(" q. Quit") fmt.Println(" q. Quit")
} }
@@ -179,6 +188,7 @@ func cliCreateContainer(reader *bufio.Reader) {
if container != nil { if container != nil {
fmt.Printf("SSH: root / %s, port %d -> 22\n", container.SSHPassword, container.SSHPort) fmt.Printf("SSH: root / %s, port %d -> 22\n", container.SSHPassword, container.SSHPort)
} }
restartWebPanelForConfigChange()
} }
func cliStartContainer(reader *bufio.Reader) { func cliStartContainer(reader *bufio.Reader) {
@@ -232,6 +242,7 @@ func cliDeleteContainer(reader *bufio.Reader) {
return return
} }
fmt.Printf("Container %s deleted\n", name) fmt.Printf("Container %s deleted\n", name)
restartWebPanelForConfigChange()
} }
func cliReinstallContainer(reader *bufio.Reader) { func cliReinstallContainer(reader *bufio.Reader) {
@@ -263,6 +274,7 @@ func cliReinstallContainer(reader *bufio.Reader) {
return return
} }
fmt.Printf("Container %s reinstalled\n", name) fmt.Printf("Container %s reinstalled\n", name)
restartWebPanelForConfigChange()
} }
func cliResetPassword(reader *bufio.Reader) { func cliResetPassword(reader *bufio.Reader) {
@@ -281,7 +293,8 @@ func cliResetPassword(reader *bufio.Reader) {
fmt.Printf("Reset failed: %v\n", err) fmt.Printf("Reset failed: %v\n", err)
return return
} }
fmt.Println("Admin password reset. Restart the web service for it to take effect.") fmt.Println("Admin password reset.")
restartWebPanelForConfigChange()
} }
func cliToggleWebPanel() { func cliToggleWebPanel() {
@@ -316,10 +329,32 @@ func isWebPanelRunning() bool {
return false return false
} }
func cliImportExistingContainers() {
fmt.Println("\n--- Import existing LXC containers ---")
fmt.Println("This imports containers from /var/lib/lxc into CLICD config.")
fmt.Println("Imported containers keep their real LXC name so Web and CLI manage the same container.")
imported, err := manager.ImportExistingClicdContainers()
if err != nil {
fmt.Printf("Import failed: %v\n", err)
return
}
if len(imported) == 0 {
fmt.Println("No new ct-* containers found to import.")
return
}
fmt.Printf("Imported %d container(s):\n", len(imported))
for _, c := range imported {
fmt.Printf(" [%d] %s [%s]\n", c.ID, c.Name, c.Status)
}
restartWebPanelForConfigChange()
}
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.")
@@ -333,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)
} }
} }
@@ -421,6 +447,14 @@ func runQuiet(name string, args ...string) {
_ = exec.Command(name, args...).Run() _ = exec.Command(name, args...).Run()
} }
func restartWebPanelForConfigChange() {
if err := restartService("clicd"); err != nil {
fmt.Printf("Web panel reload skipped: %v\n", err)
return
}
fmt.Println("Web panel reloaded to pick up config changes.")
}
func stopService(name string) error { func stopService(name string) error {
if commandExists("systemctl") { if commandExists("systemctl") {
return exec.Command("systemctl", "stop", name).Run() return exec.Command("systemctl", "stop", name).Run()
@@ -441,6 +475,16 @@ func startService(name string) error {
return fmt.Errorf("no supported service manager found") return fmt.Errorf("no supported service manager found")
} }
func restartService(name string) error {
if commandExists("systemctl") {
return exec.Command("systemctl", "restart", name).Run()
}
if commandExists("rc-service") {
return exec.Command("rc-service", name, "restart").Run()
}
return fmt.Errorf("no supported service manager found")
}
func cliShowInfo() { func cliShowInfo() {
containers, err := manager.ListContainers() containers, err := manager.ListContainers()
if err != nil { if err != nil {
+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)
} }
+99
View File
@@ -1915,6 +1915,105 @@ func (m *Manager) ListContainers() ([]config.Container, error) {
return containers, nil return containers, nil
} }
// ImportExistingClicdContainers imports existing LXC containers into the CLICD
// config. Native CLICD containers keep ct-{id}; arbitrary LXC names are stored
// in Container.LXCName so Web and CLI can manage the same imported container.
func (m *Manager) ImportExistingClicdContainers() ([]config.Container, error) {
entries, err := os.ReadDir(m.LxcPath)
if err != nil {
return nil, err
}
existingIDs := make(map[int]bool)
existingNames := make(map[string]bool)
existingLXCNames := make(map[string]bool)
maxID := config.AppConfig.NextContainerID - 1
for _, c := range config.AppConfig.Containers {
existingIDs[c.ID] = true
existingNames[c.Name] = true
existingLXCNames[c.LxcName()] = true
if c.ID > maxID {
maxID = c.ID
}
}
re := regexp.MustCompile(`^ct-([0-9]+)$`)
imported := make([]config.Container, 0)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
lxcName := entry.Name()
if existingLXCNames[lxcName] {
continue
}
id := 0
if matches := re.FindStringSubmatch(lxcName); len(matches) == 2 {
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 := lxcName
if existingNames[name] {
name = fmt.Sprintf("imported-%d", id)
}
status, err := m.GetContainerStatus(lxcName)
if err != nil || status == "" {
status = "unknown"
}
c := config.Container{
ID: id,
UUID: config.NewContainerUUID(),
Name: name,
LXCName: lxcName,
Template: "imported",
VCPU: 1,
RAMMB: 512,
DiskGB: 10,
NetworkBWMbps: 100,
MonthlyTrafficGB: 1000,
TrafficMode: "total",
Status: status,
CreatedAt: time.Now().Format(time.RFC3339),
PortMappingLimit: 2,
}
if status == "running" {
if ip, err := m.GetContainerIP(lxcName); err == nil {
c.IP = ip
}
}
config.AppConfig.Containers = append(config.AppConfig.Containers, c)
imported = append(imported, c)
existingIDs[id] = true
existingNames[name] = true
existingLXCNames[lxcName] = true
if id > maxID {
maxID = id
}
}
if len(imported) > 0 {
config.AppConfig.NextContainerID = maxID + 1
if err := config.SaveConfig(); err != nil {
return nil, err
}
}
return imported, nil
}
// ReinstallContainer reinstalls the container OS // ReinstallContainer reinstalls the container OS
func (m *Manager) ReinstallContainer(id int, templateID string) error { func (m *Manager) ReinstallContainer(id int, templateID string) error {
c := config.FindContainer(id) c := config.FindContainer(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"
} }