新添cli一键升级程序功能

This commit is contained in:
MengMengCode
2026-06-06 10:41:16 +08:00
parent 8b402fcfa1
commit 6dcd5bd06c
3 changed files with 374 additions and 107 deletions
+359 -106
View File
@@ -2,14 +2,20 @@ package cli
import ( import (
"bufio" "bufio"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"clicd/internal/config" "clicd/internal/config"
"clicd/internal/lxc" "clicd/internal/lxc"
"clicd/internal/version"
) )
var manager = lxc.NewManager() var manager = lxc.NewManager()
@@ -20,12 +26,12 @@ func Run() {
for { for {
if _, err := config.InitConfig(); err != nil { if _, err := config.InitConfig(); err != nil {
fmt.Printf("Failed to reload config: %v\n", err) fmt.Printf("重新加载配置失败: %v\n", err)
waitEnter(reader) waitEnter(reader)
} }
clearScreen() clearScreen()
printMenu() printMenu()
fmt.Print("\nSelect action [1-11,0/q]: ") fmt.Print("\n请选择操作 [1-12,0/q]: ")
input, _ := reader.ReadString('\n') input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
@@ -71,6 +77,10 @@ func Run() {
cliImportExistingContainers() cliImportExistingContainers()
waitEnter(reader) waitEnter(reader)
case "11": case "11":
clearScreen()
cliUpgradeSystem(reader)
waitEnter(reader)
case "12":
clearScreen() clearScreen()
cliUninstall(reader) cliUninstall(reader)
return return
@@ -79,60 +89,62 @@ func Run() {
cliShowInfo() cliShowInfo()
waitEnter(reader) waitEnter(reader)
case "q", "exit", "quit": case "q", "exit", "quit":
fmt.Println("Bye") fmt.Println("再见")
return return
default: default:
fmt.Println("Invalid selection") fmt.Println("无效选择")
} }
} }
} }
func printMenu() { func printMenu() {
webStatus := "start" webStatus := "启动"
if isWebPanelRunning() { if isWebPanelRunning() {
webStatus = "stop" webStatus = "停止"
} }
fmt.Println() fmt.Println()
fmt.Println(" ==========================================") fmt.Println(" ==========================================")
fmt.Println(" CLICD - LXC Container Manager") fmt.Println(" CLICD - LXC 容器管理器")
fmt.Println(" ==========================================") fmt.Println(" ==========================================")
fmt.Println() fmt.Println()
fmt.Printf(" Web panel: %s (port %d)\n", func() string { fmt.Printf(" Web 面板: %s (端口 %d)\n", func() string {
if isWebPanelRunning() { if isWebPanelRunning() {
return "running" return "运行中"
} }
return "stopped" return "已停止"
}(), config.AppConfig.Port) }(), config.AppConfig.Port)
fmt.Printf(" 当前版本: %s\n", version.Current())
fmt.Println() fmt.Println()
fmt.Println(" 1. List containers") fmt.Println(" 1. 查看容器列表")
fmt.Println(" 2. Create container") fmt.Println(" 2. 创建容器")
fmt.Println(" 3. Start container") fmt.Println(" 3. 开机容器")
fmt.Println(" 4. Stop container") fmt.Println(" 4. 关机容器")
fmt.Println(" 5. Restart container") fmt.Println(" 5. 重启容器")
fmt.Println(" 6. Delete container") fmt.Println(" 6. 删除容器")
fmt.Println(" 7. Reinstall container") fmt.Println(" 7. 重装容器系统")
fmt.Println(" 8. Reset web admin password") fmt.Println(" 8. 重置 Web 管理员密码")
fmt.Printf(" 9. %s web panel\n", webStatus) fmt.Printf(" 9. %s Web 面板\n", webStatus)
fmt.Println(" 10. Import existing LXC containers") fmt.Println(" 10. 导入现有 LXC 容器")
fmt.Println(" 11. Uninstall CLICD") fmt.Println(" 11. 检查并升级 CLICD")
fmt.Println(" 0. System info") fmt.Println(" 12. 卸载 CLICD")
fmt.Println(" q. Quit") fmt.Println(" 0. 系统信息")
fmt.Println(" q. 退出")
} }
func cliListContainers() { func cliListContainers() {
containers, err := manager.ListContainers() containers, err := manager.ListContainers()
if err != nil { if err != nil {
fmt.Printf("Failed to list containers: %v\n", err) fmt.Printf("获取容器列表失败: %v\n", err)
return return
} }
if len(containers) == 0 { if len(containers) == 0 {
fmt.Println("\nNo containers") fmt.Println("\n暂无容器")
return return
} }
fmt.Println() fmt.Println()
fmt.Printf("%-18s %-10s %-18s %-6s %-10s %-10s %-16s\n", "Name", "Status", "Template", "vCPU", "RAM(MB)", "Disk(GB)", "SSH") fmt.Printf("%-18s %-10s %-18s %-6s %-10s %-10s %-16s\n", "名称", "状态", "镜像", "vCPU", "内存(MB)", "磁盘(GB)", "SSH")
fmt.Println(strings.Repeat("-", 94)) fmt.Println(strings.Repeat("-", 94))
for _, c := range containers { for _, c := range containers {
ssh := "-" ssh := "-"
@@ -145,23 +157,23 @@ func cliListContainers() {
} }
func cliCreateContainer(reader *bufio.Reader) { func cliCreateContainer(reader *bufio.Reader) {
fmt.Println("\n--- Create container ---") fmt.Println("\n--- 创建容器 ---")
name := promptString(reader, "Container name", "") name := promptString(reader, "容器名称", "")
if name == "" { if name == "" {
fmt.Println("Container name is required") fmt.Println("容器名称不能为空")
return return
} }
templates := lxc.GetTemplates() templates := lxc.GetTemplates()
fmt.Println("\nAvailable templates:") fmt.Println("\n可用镜像:")
for i, template := range templates { for i, template := range templates {
fmt.Printf(" %d. %s\n", i+1, template.Name) fmt.Printf(" %d. %s\n", i+1, template.Name)
} }
tmplIdx := promptInt(reader, fmt.Sprintf("Template [1-%d]", len(templates)), 1) tmplIdx := promptInt(reader, fmt.Sprintf("镜像 [1-%d]", len(templates)), 1)
if tmplIdx < 1 || tmplIdx > len(templates) { if tmplIdx < 1 || tmplIdx > len(templates) {
fmt.Println("Invalid template selection") fmt.Println("镜像选择无效")
return return
} }
@@ -169,22 +181,22 @@ func cliCreateContainer(reader *bufio.Reader) {
Name: name, Name: name,
TemplateID: templates[tmplIdx-1].ID, TemplateID: templates[tmplIdx-1].ID,
VCPU: promptFloat(reader, "vCPU", 1), VCPU: promptFloat(reader, "vCPU", 1),
RAMMB: promptInt(reader, "Memory (MB)", 512), RAMMB: promptInt(reader, "内存 (MB)", 512),
DiskGB: promptInt(reader, "Disk (GB)", 10), DiskGB: promptInt(reader, "磁盘 (GB)", 10),
NetworkBWMbps: promptInt(reader, "Network bandwidth (Mbps)", 100), NetworkBWMbps: promptInt(reader, "网络带宽 (Mbps)", 100),
MonthlyTrafficGB: promptInt(reader, "Monthly traffic (GB)", 1000), MonthlyTrafficGB: promptInt(reader, "月流量 (GB)", 1000),
IOSpeedMBps: promptInt(reader, "IO speed (MB/s)", 500), IOSpeedMBps: promptInt(reader, "IO 速度 (MB/s)", 500),
ExtraPorts: promptPortList(reader, "Extra NAT ports, comma separated"), ExtraPorts: promptPortList(reader, "额外 NAT 端口,多个用逗号分隔"),
} }
fmt.Printf("\nCreating container %s ...\n", name) fmt.Printf("\n正在创建容器 %s ...\n", name)
if err := manager.CreateContainer(cfg); err != nil { if err := manager.CreateContainer(cfg); err != nil {
fmt.Printf("Create failed: %v\n", err) fmt.Printf("创建失败: %v\n", err)
return return
} }
container := config.FindContainerByName(name) container := config.FindContainerByName(name)
fmt.Printf("Container %s created successfully\n", name) fmt.Printf("容器 %s 创建成功\n", name)
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)
} }
@@ -192,126 +204,366 @@ func cliCreateContainer(reader *bufio.Reader) {
} }
func cliStartContainer(reader *bufio.Reader) { func cliStartContainer(reader *bufio.Reader) {
id, name := selectContainer(reader, "start") id, name := selectContainer(reader, "开机")
if id == 0 { if id == 0 {
return return
} }
if err := manager.StartContainer(id); err != nil { if err := manager.StartContainer(id); err != nil {
fmt.Printf("Start failed: %v\n", err) fmt.Printf("开机失败: %v\n", err)
return return
} }
fmt.Printf("Container %s started\n", name) fmt.Printf("容器 %s 已开机\n", name)
} }
func cliStopContainer(reader *bufio.Reader) { func cliStopContainer(reader *bufio.Reader) {
id, name := selectContainer(reader, "stop") id, name := selectContainer(reader, "关机")
if id == 0 { if id == 0 {
return return
} }
if err := manager.StopContainer(id); err != nil { if err := manager.StopContainer(id); err != nil {
fmt.Printf("Stop failed: %v\n", err) fmt.Printf("关机失败: %v\n", err)
return return
} }
fmt.Printf("Container %s stopped\n", name) fmt.Printf("容器 %s 已关机\n", name)
} }
func cliRestartContainer(reader *bufio.Reader) { func cliRestartContainer(reader *bufio.Reader) {
id, name := selectContainer(reader, "restart") id, name := selectContainer(reader, "重启")
if id == 0 { if id == 0 {
return return
} }
if err := manager.RestartContainer(id); err != nil { if err := manager.RestartContainer(id); err != nil {
fmt.Printf("Restart failed: %v\n", err) fmt.Printf("重启失败: %v\n", err)
return return
} }
fmt.Printf("Container %s restarted\n", name) fmt.Printf("容器 %s 已重启\n", name)
} }
func cliDeleteContainer(reader *bufio.Reader) { func cliDeleteContainer(reader *bufio.Reader) {
id, name := selectContainer(reader, "delete") id, name := selectContainer(reader, "删除")
if id == 0 { if id == 0 {
return return
} }
confirm := promptString(reader, fmt.Sprintf("Delete container %s? Type yes", name), "no") confirm := promptString(reader, fmt.Sprintf("确认删除容器 %s?输入 yes 继续", name), "no")
if strings.ToLower(confirm) != "yes" { if strings.ToLower(confirm) != "yes" {
fmt.Println("Canceled") fmt.Println("已取消")
return return
} }
if err := manager.DestroyContainer(id); err != nil { if err := manager.DestroyContainer(id); err != nil {
fmt.Printf("Delete failed: %v\n", err) fmt.Printf("删除失败: %v\n", err)
return return
} }
fmt.Printf("Container %s deleted\n", name) fmt.Printf("容器 %s 已删除\n", name)
restartWebPanelForConfigChange() restartWebPanelForConfigChange()
} }
func cliReinstallContainer(reader *bufio.Reader) { func cliReinstallContainer(reader *bufio.Reader) {
id, name := selectContainer(reader, "reinstall") id, name := selectContainer(reader, "重装")
if id == 0 { if id == 0 {
return return
} }
templates := lxc.GetTemplates() templates := lxc.GetTemplates()
fmt.Println("\nAvailable templates:") fmt.Println("\n可用镜像:")
for i, template := range templates { for i, template := range templates {
fmt.Printf(" %d. %s\n", i+1, template.Name) fmt.Printf(" %d. %s\n", i+1, template.Name)
} }
tmplIdx := promptInt(reader, fmt.Sprintf("Template [1-%d]", len(templates)), 1) tmplIdx := promptInt(reader, fmt.Sprintf("镜像 [1-%d]", len(templates)), 1)
if tmplIdx < 1 || tmplIdx > len(templates) { if tmplIdx < 1 || tmplIdx > len(templates) {
fmt.Println("Invalid template selection") fmt.Println("镜像选择无效")
return return
} }
confirm := promptString(reader, fmt.Sprintf("Reinstall container %s? Type yes", name), "no") confirm := promptString(reader, fmt.Sprintf("确认重装容器 %s?输入 yes 继续", name), "no")
if strings.ToLower(confirm) != "yes" { if strings.ToLower(confirm) != "yes" {
fmt.Println("Canceled") fmt.Println("已取消")
return return
} }
if err := manager.ReinstallContainer(id, templates[tmplIdx-1].ID); err != nil { if err := manager.ReinstallContainer(id, templates[tmplIdx-1].ID); err != nil {
fmt.Printf("Reinstall failed: %v\n", err) fmt.Printf("重装失败: %v\n", err)
return return
} }
fmt.Printf("Container %s reinstalled\n", name) fmt.Printf("容器 %s 已重装\n", name)
restartWebPanelForConfigChange() restartWebPanelForConfigChange()
} }
func cliResetPassword(reader *bufio.Reader) { func cliResetPassword(reader *bufio.Reader) {
newPass := promptString(reader, "New admin password (at least 6 chars)", "") newPass := promptString(reader, "新的管理员密码(至少 6 位)", "")
if len(newPass) < 6 { if len(newPass) < 6 {
fmt.Println("Password must be at least 6 chars") fmt.Println("密码至少需要 6 位")
return return
} }
confirm := promptString(reader, "Confirm password", "") confirm := promptString(reader, "确认密码", "")
if newPass != confirm { if newPass != confirm {
fmt.Println("Passwords do not match") fmt.Println("两次输入的密码不一致")
return return
} }
if err := config.ResetAdminPassword(newPass); err != nil { if err := config.ResetAdminPassword(newPass); err != nil {
fmt.Printf("Reset failed: %v\n", err) fmt.Printf("重置失败: %v\n", err)
return return
} }
fmt.Println("Admin password reset.") fmt.Println("管理员密码已重置。")
restartWebPanelForConfigChange() restartWebPanelForConfigChange()
} }
func cliToggleWebPanel() { func cliToggleWebPanel() {
if isWebPanelRunning() { if isWebPanelRunning() {
if err := stopService("clicd"); err != nil { if err := stopService("clicd"); err != nil {
fmt.Printf("Failed to stop web panel: %v\n", err) fmt.Printf("停止 Web 面板失败: %v\n", err)
return return
} }
fmt.Println("Web panel stopped. LXC containers are not affected.") fmt.Println("Web 面板已停止,LXC 容器不会受影响。")
return return
} }
if err := startService("clicd"); err != nil { if err := startService("clicd"); err != nil {
fmt.Printf("Failed to start web panel: %v\n", err) fmt.Printf("启动 Web 面板失败: %v\n", err)
return return
} }
fmt.Println("Web panel started") fmt.Println("Web 面板已启动")
}
type githubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
HTMLURL string `json:"html_url"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
}
func cliUpgradeSystem(reader *bufio.Reader) {
fmt.Println("\n--- 检查并升级 CLICD ---")
fmt.Println("升级只会替换 /usr/local/bin/clicd,并保留 /root/.clicd 里的配置、容器数据和任务记录。")
if os.Geteuid() != 0 {
fmt.Println("升级需要 root 权限。请使用: sudo clicd cli")
return
}
repo := strings.TrimSpace(os.Getenv("CLICD_REPO"))
if repo == "" {
repo = version.Repo
}
current := version.Current()
fmt.Printf("当前版本: %s\n", current)
fmt.Printf("检查仓库: https://github.com/%s\n", repo)
release, err := fetchLatestRelease(repo)
if err != nil {
fmt.Printf("检查 GitHub 最新版本失败: %v\n", err)
return
}
latest := strings.TrimSpace(release.TagName)
if latest == "" {
fmt.Println("GitHub Release 没有 tag_name,无法判断最新版本。")
return
}
fmt.Printf("最新版本: %s\n", latest)
if release.HTMLURL != "" {
fmt.Printf("发布页面: %s\n", release.HTMLURL)
}
assetURL := findReleaseAsset(release, "clicd-linux-amd64.tar.gz")
if assetURL == "" {
fmt.Println("最新 Release 没有找到 clicd-linux-amd64.tar.gz,无法自动升级。")
return
}
if sameVersion(current, latest) {
fmt.Println("当前已经是最新版本。")
confirm := promptString(reader, "是否仍然重新安装最新版本?输入 reinstall 继续", "no")
if strings.ToLower(confirm) != "reinstall" {
fmt.Println("已取消。")
return
}
} else {
confirm := promptString(reader, "输入 upgrade 开始升级", "no")
if strings.ToLower(confirm) != "upgrade" {
fmt.Println("已取消。")
return
}
}
if err := upgradeFromReleaseAsset(assetURL, latest); err != nil {
fmt.Printf("升级失败: %v\n", err)
return
}
fmt.Printf("升级完成: %s -> %s\n", current, latest)
fmt.Println("原有数据已保留,Web 服务已重启。")
}
func fetchLatestRelease(repo string) (*githubRelease, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "clicd-updater/"+version.Current())
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, fmt.Errorf("GitHub API 返回 %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
var release githubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return nil, err
}
return &release, nil
}
func findReleaseAsset(release *githubRelease, name string) string {
for _, asset := range release.Assets {
if asset.Name == name && asset.BrowserDownloadURL != "" {
return asset.BrowserDownloadURL
}
}
return ""
}
func upgradeFromReleaseAsset(assetURL, latest string) error {
tmpDir, err := os.MkdirTemp("", "clicd-upgrade-*")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
archivePath := filepath.Join(tmpDir, "clicd-linux-amd64.tar.gz")
fmt.Println("正在下载升级包...")
if err := downloadFile(assetURL, archivePath); err != nil {
return err
}
fmt.Println("正在解压升级包...")
if out, err := exec.Command("tar", "-xzf", archivePath, "-C", tmpDir).CombinedOutput(); err != nil {
return fmt.Errorf("解压失败: %v, output: %s", err, string(out))
}
newBinary, err := findFile(tmpDir, "clicd")
if err != nil {
return err
}
backupDir := "/root/clicd-backups"
if err := os.MkdirAll(backupDir, 0700); err != nil {
return err
}
backupPath := filepath.Join(backupDir, fmt.Sprintf("clicd.%s.%s", strings.TrimPrefix(latest, "v"), time.Now().Format("20060102-150405")))
if _, err := os.Stat("/usr/local/bin/clicd"); err == nil {
if err := copyFile("/usr/local/bin/clicd", backupPath, 0755); err != nil {
return fmt.Errorf("备份旧二进制失败: %w", err)
}
fmt.Printf("旧版本已备份: %s\n", backupPath)
}
fmt.Println("正在替换二进制...")
if err := stopService("clicd"); err != nil {
fmt.Printf("停止 Web 服务失败,继续尝试替换: %v\n", err)
}
tmpBin := "/usr/local/bin/clicd.new"
if err := copyFile(newBinary, tmpBin, 0755); err != nil {
return err
}
if err := os.Rename(tmpBin, "/usr/local/bin/clicd"); err != nil {
return err
}
if err := os.Chmod("/usr/local/bin/clicd", 0755); err != nil {
return err
}
if err := restartService("clicd"); err != nil {
return fmt.Errorf("二进制已替换,但重启 Web 服务失败: %w", err)
}
return nil
}
func downloadFile(url, dest string) error {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "clicd-updater/"+version.Current())
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载失败,HTTP %s", resp.Status)
}
out, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func findFile(root, name string) (string, error) {
var found string
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if d.Name() == name {
found = path
return filepath.SkipAll
}
return nil
})
if err != nil {
return "", err
}
if found == "" {
return "", fmt.Errorf("升级包内未找到 clicd 二进制")
}
return found, nil
}
func copyFile(src, dst string, mode os.FileMode) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
if err != nil {
return err
}
if _, err := io.Copy(out, in); err != nil {
out.Close()
return err
}
if err := out.Close(); err != nil {
return err
}
return os.Chmod(dst, mode)
}
func sameVersion(current, latest string) bool {
c := strings.TrimPrefix(strings.TrimSpace(strings.ToLower(current)), "v")
l := strings.TrimPrefix(strings.TrimSpace(strings.ToLower(latest)), "v")
return c != "" && c == l
} }
func isWebPanelRunning() bool { func isWebPanelRunning() bool {
@@ -330,21 +582,21 @@ func isWebPanelRunning() bool {
} }
func cliImportExistingContainers() { func cliImportExistingContainers() {
fmt.Println("\n--- Import existing LXC containers ---") fmt.Println("\n--- 导入现有 LXC 容器 ---")
fmt.Println("This imports containers from /var/lib/lxc into CLICD config.") fmt.Println(" /var/lib/lxc 里的容器导入 CLICD 配置。")
fmt.Println("Imported containers keep their real LXC name so Web and CLI manage the same container.") fmt.Println("导入后会保留真实 LXC 名称,Web CLI 都能管理同一个容器。")
imported, err := manager.ImportExistingClicdContainers() imported, err := manager.ImportExistingClicdContainers()
if err != nil { if err != nil {
fmt.Printf("Import failed: %v\n", err) fmt.Printf("导入失败: %v\n", err)
return return
} }
if len(imported) == 0 { if len(imported) == 0 {
fmt.Println("No new ct-* containers found to import.") fmt.Println("没有发现新的 ct-* 容器。")
return return
} }
fmt.Printf("Imported %d container(s):\n", len(imported)) fmt.Printf("已导入 %d 个容器:\n", len(imported))
for _, c := range imported { for _, c := range imported {
fmt.Printf(" [%d] %s [%s]\n", c.ID, c.Name, c.Status) fmt.Printf(" [%d] %s [%s]\n", c.ID, c.Name, c.Status)
} }
@@ -352,19 +604,19 @@ func cliImportExistingContainers() {
} }
func cliUninstall(reader *bufio.Reader) { func cliUninstall(reader *bufio.Reader) {
fmt.Println("\n--- Uninstall CLICD ---") fmt.Println("\n--- 卸载 CLICD ---")
fmt.Println("This removes the CLICD service and /usr/local/bin/clicd.") fmt.Println("将删除 CLICD 服务和 /usr/local/bin/clicd")
fmt.Println("This also deletes /root/.clicd, all LXC containers under /var/lib/lxc, and LXC image cache under /var/cache/lxc.") fmt.Println("同时会删除 /root/.clicd、/var/lib/lxc 下全部 LXC 容器,以及 /var/cache/lxc 镜像缓存。")
if os.Geteuid() != 0 { if os.Geteuid() != 0 {
fmt.Println("Uninstall must be run as root.") fmt.Println("卸载需要 root 权限。")
fmt.Println("Run: sudo clicd cli --no-web") fmt.Println("请运行: sudo clicd cli --no-web")
return return
} }
confirm := promptString(reader, "Type uninstall to continue", "no") confirm := promptString(reader, "输入 uninstall 继续卸载", "no")
if strings.ToLower(confirm) != "uninstall" { if strings.ToLower(confirm) != "uninstall" {
fmt.Println("Canceled") fmt.Println("已取消")
return return
} }
@@ -381,8 +633,8 @@ func cliUninstall(reader *bufio.Reader) {
reloadSysctl() reloadSysctl()
fmt.Println() fmt.Println()
fmt.Println("CLICD has been uninstalled.") fmt.Println("CLICD 已卸载。")
fmt.Println("Removed service, binary, config, containers, and LXC image cache.") fmt.Println("服务、二进制、配置、容器和 LXC 镜像缓存均已删除。")
} }
func destroyAllLXCContainers() { func destroyAllLXCContainers() {
@@ -509,10 +761,10 @@ func runQuiet(name string, args ...string) {
func restartWebPanelForConfigChange() { func restartWebPanelForConfigChange() {
if err := restartService("clicd"); err != nil { if err := restartService("clicd"); err != nil {
fmt.Printf("Web panel reload skipped: %v\n", err) fmt.Printf("Web 面板重载跳过: %v\n", err)
return return
} }
fmt.Println("Web panel reloaded to pick up config changes.") fmt.Println("Web 面板已重载并应用配置变更。")
} }
func stopService(name string) error { func stopService(name string) error {
@@ -548,7 +800,7 @@ func restartService(name string) error {
func cliShowInfo() { func cliShowInfo() {
containers, err := manager.ListContainers() containers, err := manager.ListContainers()
if err != nil { if err != nil {
fmt.Printf("Failed to read container status: %v\n", err) fmt.Printf("读取容器状态失败: %v\n", err)
} }
total := len(containers) total := len(containers)
@@ -559,43 +811,44 @@ func cliShowInfo() {
} }
} }
fmt.Println("\n--- System info ---") fmt.Println("\n--- 系统信息 ---")
fmt.Printf("Web port: %d\n", config.AppConfig.Port) fmt.Printf("CLICD 版本: %s\n", version.Current())
fmt.Printf("Admin user: %s\n", config.AppConfig.AdminUser) fmt.Printf("Web 端口: %d\n", config.AppConfig.Port)
fmt.Printf("Containers: %d\n", total) fmt.Printf("管理员用户: %s\n", config.AppConfig.AdminUser)
fmt.Printf("Running: %d\n", running) fmt.Printf("容器总数: %d\n", total)
fmt.Printf("Stopped: %d\n", total-running) fmt.Printf("运行中: %d\n", running)
fmt.Printf("已停止: %d\n", total-running)
if hostname, err := os.Hostname(); err == nil { if hostname, err := os.Hostname(); err == nil {
fmt.Printf("Hostname: %s\n", hostname) fmt.Printf("主机名: %s\n", hostname)
} }
cmd := exec.Command("lxc-info", "--version") cmd := exec.Command("lxc-info", "--version")
output, err := cmd.Output() output, err := cmd.Output()
if err == nil { if err == nil {
fmt.Printf("LXC version: %s", string(output)) fmt.Printf("LXC 版本: %s", string(output))
} }
} }
func selectContainer(reader *bufio.Reader, action string) (int, string) { func selectContainer(reader *bufio.Reader, action string) (int, string) {
containers, err := manager.ListContainers() containers, err := manager.ListContainers()
if err != nil { if err != nil {
fmt.Printf("Failed to list containers: %v\n", err) fmt.Printf("获取容器列表失败: %v\n", err)
return 0, "" return 0, ""
} }
if len(containers) == 0 { if len(containers) == 0 {
fmt.Println("No containers available") fmt.Println("暂无可用容器")
return 0, "" return 0, ""
} }
fmt.Printf("\n--- Select container to %s ---\n", action) fmt.Printf("\n--- 选择要%s的容器 ---\n", action)
for i, container := range containers { for i, container := range containers {
fmt.Printf(" %d. [%d] %s [%s]\n", i+1, container.ID, container.Name, container.Status) fmt.Printf(" %d. [%d] %s [%s]\n", i+1, container.ID, container.Name, container.Status)
} }
idx := promptInt(reader, "Container", 0) idx := promptInt(reader, "容器", 0)
if idx < 1 || idx > len(containers) { if idx < 1 || idx > len(containers) {
fmt.Println("Invalid selection") fmt.Println("选择无效")
return 0, "" return 0, ""
} }
@@ -641,7 +894,7 @@ func clearScreen() {
} }
func waitEnter(reader *bufio.Reader) { func waitEnter(reader *bufio.Reader) {
fmt.Print("\nPress Enter to return to menu...") fmt.Print("\n Enter 返回菜单...")
reader.ReadString('\n') reader.ReadString('\n')
} }
@@ -655,7 +908,7 @@ func promptPortList(reader *bufio.Reader, label string) []int {
for _, part := range strings.Split(input, ",") { for _, part := range strings.Split(input, ",") {
value, err := strconv.Atoi(strings.TrimSpace(part)) value, err := strconv.Atoi(strings.TrimSpace(part))
if err != nil || value <= 0 || value > 65535 { if err != nil || value <= 0 || value > 65535 {
fmt.Printf("Ignoring invalid port: %s\n", strings.TrimSpace(part)) fmt.Printf("忽略无效端口: %s\n", strings.TrimSpace(part))
continue continue
} }
ports = append(ports, value) ports = append(ports, value)
+13
View File
@@ -0,0 +1,13 @@
package version
var (
Version = "dev"
Repo = "MengMengCode/CLICD"
)
func Current() string {
if Version == "" {
return "dev"
}
return Version
}
+2 -1
View File
@@ -52,7 +52,8 @@ go mod tidy
go mod download go mod download
# Build for Linux amd64 # Build for Linux amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o "$BUILD_DIR/clicd" . BUILD_VERSION="${CLICD_VERSION:-dev}"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w -X clicd/internal/version.Version=${BUILD_VERSION}" -o "$BUILD_DIR/clicd" .
echo "Go backend built successfully" echo "Go backend built successfully"