Compare commits

...

2 Commits

Author SHA1 Message Date
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
4 changed files with 160 additions and 3 deletions
+1
View File
@@ -62,3 +62,4 @@ backend/tmp/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
linux.txt
+56 -3
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,6 +329,28 @@ 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.")
@@ -421,6 +456,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 +484,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)