mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
561 lines
14 KiB
Go
561 lines
14 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"clicd/internal/config"
|
|
"clicd/internal/lxc"
|
|
)
|
|
|
|
var manager = lxc.NewManager()
|
|
|
|
// Run starts the CLI interface.
|
|
func Run() {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
for {
|
|
clearScreen()
|
|
printMenu()
|
|
fmt.Print("\nSelect action [1-10,0/q]: ")
|
|
input, _ := reader.ReadString('\n')
|
|
input = strings.TrimSpace(input)
|
|
|
|
switch strings.ToLower(input) {
|
|
case "1":
|
|
clearScreen()
|
|
cliListContainers()
|
|
waitEnter(reader)
|
|
case "2":
|
|
clearScreen()
|
|
cliCreateContainer(reader)
|
|
waitEnter(reader)
|
|
case "3":
|
|
clearScreen()
|
|
cliStartContainer(reader)
|
|
waitEnter(reader)
|
|
case "4":
|
|
clearScreen()
|
|
cliStopContainer(reader)
|
|
waitEnter(reader)
|
|
case "5":
|
|
clearScreen()
|
|
cliRestartContainer(reader)
|
|
waitEnter(reader)
|
|
case "6":
|
|
clearScreen()
|
|
cliDeleteContainer(reader)
|
|
waitEnter(reader)
|
|
case "7":
|
|
clearScreen()
|
|
cliReinstallContainer(reader)
|
|
waitEnter(reader)
|
|
case "8":
|
|
clearScreen()
|
|
cliResetPassword(reader)
|
|
waitEnter(reader)
|
|
case "9":
|
|
clearScreen()
|
|
cliToggleWebPanel()
|
|
waitEnter(reader)
|
|
case "10":
|
|
clearScreen()
|
|
cliUninstall(reader)
|
|
return
|
|
case "0":
|
|
clearScreen()
|
|
cliShowInfo()
|
|
waitEnter(reader)
|
|
case "q", "exit", "quit":
|
|
fmt.Println("Bye")
|
|
return
|
|
default:
|
|
fmt.Println("Invalid selection")
|
|
}
|
|
}
|
|
}
|
|
|
|
func printMenu() {
|
|
webStatus := "start"
|
|
if isWebPanelRunning() {
|
|
webStatus = "stop"
|
|
}
|
|
fmt.Println()
|
|
fmt.Println(" ==========================================")
|
|
fmt.Println(" CLICD - LXC Container Manager")
|
|
fmt.Println(" ==========================================")
|
|
fmt.Println()
|
|
fmt.Printf(" Web panel: %s (port %d)\n", func() string {
|
|
if isWebPanelRunning() {
|
|
return "running"
|
|
}
|
|
return "stopped"
|
|
}(), config.AppConfig.Port)
|
|
fmt.Println()
|
|
fmt.Println(" 1. List containers")
|
|
fmt.Println(" 2. Create container")
|
|
fmt.Println(" 3. Start container")
|
|
fmt.Println(" 4. Stop container")
|
|
fmt.Println(" 5. Restart container")
|
|
fmt.Println(" 6. Delete container")
|
|
fmt.Println(" 7. Reinstall container")
|
|
fmt.Println(" 8. Reset web admin password")
|
|
fmt.Printf(" 9. %s web panel\n", webStatus)
|
|
fmt.Println(" 10. Uninstall CLICD")
|
|
fmt.Println(" 0. System info")
|
|
fmt.Println(" q. Quit")
|
|
}
|
|
|
|
func cliListContainers() {
|
|
containers, err := manager.ListContainers()
|
|
if err != nil {
|
|
fmt.Printf("Failed to list containers: %v\n", err)
|
|
return
|
|
}
|
|
|
|
if len(containers) == 0 {
|
|
fmt.Println("\nNo containers")
|
|
return
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Printf("%-18s %-10s %-18s %-6s %-10s %-10s %-16s\n", "Name", "Status", "Template", "vCPU", "RAM(MB)", "Disk(GB)", "SSH")
|
|
fmt.Println(strings.Repeat("-", 94))
|
|
for _, c := range containers {
|
|
ssh := "-"
|
|
if c.SSHPort > 0 {
|
|
ssh = fmt.Sprintf("%d->22", c.SSHPort)
|
|
}
|
|
fmt.Printf("%-18s %-10s %-18s %-6.2f %-10d %-10d %-16s\n",
|
|
c.Name, c.Status, c.Template, c.VCPU, c.RAMMB, c.DiskGB, ssh)
|
|
}
|
|
}
|
|
|
|
func cliCreateContainer(reader *bufio.Reader) {
|
|
fmt.Println("\n--- Create container ---")
|
|
|
|
name := promptString(reader, "Container name", "")
|
|
if name == "" {
|
|
fmt.Println("Container name is required")
|
|
return
|
|
}
|
|
|
|
templates := lxc.GetTemplates()
|
|
fmt.Println("\nAvailable templates:")
|
|
for i, template := range templates {
|
|
fmt.Printf(" %d. %s\n", i+1, template.Name)
|
|
}
|
|
|
|
tmplIdx := promptInt(reader, fmt.Sprintf("Template [1-%d]", len(templates)), 1)
|
|
if tmplIdx < 1 || tmplIdx > len(templates) {
|
|
fmt.Println("Invalid template selection")
|
|
return
|
|
}
|
|
|
|
cfg := lxc.ContainerConfig{
|
|
Name: name,
|
|
TemplateID: templates[tmplIdx-1].ID,
|
|
VCPU: promptFloat(reader, "vCPU", 1),
|
|
RAMMB: promptInt(reader, "Memory (MB)", 512),
|
|
DiskGB: promptInt(reader, "Disk (GB)", 10),
|
|
NetworkBWMbps: promptInt(reader, "Network bandwidth (Mbps)", 100),
|
|
MonthlyTrafficGB: promptInt(reader, "Monthly traffic (GB)", 1000),
|
|
IOSpeedMBps: promptInt(reader, "IO speed (MB/s)", 500),
|
|
ExtraPorts: promptPortList(reader, "Extra NAT ports, comma separated"),
|
|
}
|
|
|
|
fmt.Printf("\nCreating container %s ...\n", name)
|
|
if err := manager.CreateContainer(cfg); err != nil {
|
|
fmt.Printf("Create failed: %v\n", err)
|
|
return
|
|
}
|
|
|
|
container := config.FindContainerByName(name)
|
|
fmt.Printf("Container %s created successfully\n", name)
|
|
if container != nil {
|
|
fmt.Printf("SSH: root / %s, port %d -> 22\n", container.SSHPassword, container.SSHPort)
|
|
}
|
|
}
|
|
|
|
func cliStartContainer(reader *bufio.Reader) {
|
|
id, name := selectContainer(reader, "start")
|
|
if id == 0 {
|
|
return
|
|
}
|
|
if err := manager.StartContainer(id); err != nil {
|
|
fmt.Printf("Start failed: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Printf("Container %s started\n", name)
|
|
}
|
|
|
|
func cliStopContainer(reader *bufio.Reader) {
|
|
id, name := selectContainer(reader, "stop")
|
|
if id == 0 {
|
|
return
|
|
}
|
|
if err := manager.StopContainer(id); err != nil {
|
|
fmt.Printf("Stop failed: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Printf("Container %s stopped\n", name)
|
|
}
|
|
|
|
func cliRestartContainer(reader *bufio.Reader) {
|
|
id, name := selectContainer(reader, "restart")
|
|
if id == 0 {
|
|
return
|
|
}
|
|
if err := manager.RestartContainer(id); err != nil {
|
|
fmt.Printf("Restart failed: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Printf("Container %s restarted\n", name)
|
|
}
|
|
|
|
func cliDeleteContainer(reader *bufio.Reader) {
|
|
id, name := selectContainer(reader, "delete")
|
|
if id == 0 {
|
|
return
|
|
}
|
|
confirm := promptString(reader, fmt.Sprintf("Delete container %s? Type yes", name), "no")
|
|
if strings.ToLower(confirm) != "yes" {
|
|
fmt.Println("Canceled")
|
|
return
|
|
}
|
|
if err := manager.DestroyContainer(id); err != nil {
|
|
fmt.Printf("Delete failed: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Printf("Container %s deleted\n", name)
|
|
}
|
|
|
|
func cliReinstallContainer(reader *bufio.Reader) {
|
|
id, name := selectContainer(reader, "reinstall")
|
|
if id == 0 {
|
|
return
|
|
}
|
|
|
|
templates := lxc.GetTemplates()
|
|
fmt.Println("\nAvailable templates:")
|
|
for i, template := range templates {
|
|
fmt.Printf(" %d. %s\n", i+1, template.Name)
|
|
}
|
|
|
|
tmplIdx := promptInt(reader, fmt.Sprintf("Template [1-%d]", len(templates)), 1)
|
|
if tmplIdx < 1 || tmplIdx > len(templates) {
|
|
fmt.Println("Invalid template selection")
|
|
return
|
|
}
|
|
|
|
confirm := promptString(reader, fmt.Sprintf("Reinstall container %s? Type yes", name), "no")
|
|
if strings.ToLower(confirm) != "yes" {
|
|
fmt.Println("Canceled")
|
|
return
|
|
}
|
|
|
|
if err := manager.ReinstallContainer(id, templates[tmplIdx-1].ID); err != nil {
|
|
fmt.Printf("Reinstall failed: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Printf("Container %s reinstalled\n", name)
|
|
}
|
|
|
|
func cliResetPassword(reader *bufio.Reader) {
|
|
newPass := promptString(reader, "New admin password (at least 6 chars)", "")
|
|
if len(newPass) < 6 {
|
|
fmt.Println("Password must be at least 6 chars")
|
|
return
|
|
}
|
|
confirm := promptString(reader, "Confirm password", "")
|
|
if newPass != confirm {
|
|
fmt.Println("Passwords do not match")
|
|
return
|
|
}
|
|
|
|
if err := config.ResetAdminPassword(newPass); err != nil {
|
|
fmt.Printf("Reset failed: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Println("Admin password reset. Restart the web service for it to take effect.")
|
|
}
|
|
|
|
func cliToggleWebPanel() {
|
|
if isWebPanelRunning() {
|
|
if err := stopService("clicd"); err != nil {
|
|
fmt.Printf("Failed to stop web panel: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Println("Web panel stopped. LXC containers are not affected.")
|
|
return
|
|
}
|
|
|
|
if err := startService("clicd"); err != nil {
|
|
fmt.Printf("Failed to start web panel: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Println("Web panel started")
|
|
}
|
|
|
|
func isWebPanelRunning() bool {
|
|
if commandExists("systemctl") {
|
|
cmd := exec.Command("systemctl", "is-active", "clicd")
|
|
output, err := cmd.Output()
|
|
if err == nil && strings.TrimSpace(string(output)) == "active" {
|
|
return true
|
|
}
|
|
}
|
|
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() {
|
|
containers, err := manager.ListContainers()
|
|
if err != nil {
|
|
fmt.Printf("Failed to read container status: %v\n", err)
|
|
}
|
|
|
|
total := len(containers)
|
|
running := 0
|
|
for _, container := range containers {
|
|
if container.Status == "running" {
|
|
running++
|
|
}
|
|
}
|
|
|
|
fmt.Println("\n--- System info ---")
|
|
fmt.Printf("Web port: %d\n", config.AppConfig.Port)
|
|
fmt.Printf("Admin user: %s\n", config.AppConfig.AdminUser)
|
|
fmt.Printf("Containers: %d\n", total)
|
|
fmt.Printf("Running: %d\n", running)
|
|
fmt.Printf("Stopped: %d\n", total-running)
|
|
|
|
if hostname, err := os.Hostname(); err == nil {
|
|
fmt.Printf("Hostname: %s\n", hostname)
|
|
}
|
|
|
|
cmd := exec.Command("lxc-info", "--version")
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
fmt.Printf("LXC version: %s", string(output))
|
|
}
|
|
}
|
|
|
|
func selectContainer(reader *bufio.Reader, action string) (int, string) {
|
|
containers, err := manager.ListContainers()
|
|
if err != nil {
|
|
fmt.Printf("Failed to list containers: %v\n", err)
|
|
return 0, ""
|
|
}
|
|
if len(containers) == 0 {
|
|
fmt.Println("No containers available")
|
|
return 0, ""
|
|
}
|
|
|
|
fmt.Printf("\n--- Select container to %s ---\n", action)
|
|
for i, container := range containers {
|
|
fmt.Printf(" %d. [%d] %s [%s]\n", i+1, container.ID, container.Name, container.Status)
|
|
}
|
|
|
|
idx := promptInt(reader, "Container", 0)
|
|
if idx < 1 || idx > len(containers) {
|
|
fmt.Println("Invalid selection")
|
|
return 0, ""
|
|
}
|
|
|
|
c := containers[idx-1]
|
|
return c.ID, c.Name
|
|
}
|
|
|
|
func promptString(reader *bufio.Reader, label string, fallback string) string {
|
|
if fallback == "" {
|
|
fmt.Printf("%s: ", label)
|
|
} else {
|
|
fmt.Printf("%s [%s]: ", label, fallback)
|
|
}
|
|
|
|
input, _ := reader.ReadString('\n')
|
|
input = strings.TrimSpace(input)
|
|
if input == "" {
|
|
return fallback
|
|
}
|
|
return input
|
|
}
|
|
|
|
func promptInt(reader *bufio.Reader, label string, fallback int) int {
|
|
input := promptString(reader, label, strconv.Itoa(fallback))
|
|
value, err := strconv.Atoi(input)
|
|
if err != nil || value < 0 {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func promptFloat(reader *bufio.Reader, label string, fallback float64) float64 {
|
|
input := promptString(reader, label, strconv.FormatFloat(fallback, 'f', -1, 64))
|
|
value, err := strconv.ParseFloat(input, 64)
|
|
if err != nil || value < 0 {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func clearScreen() {
|
|
fmt.Print("\033[H\033[2J")
|
|
}
|
|
|
|
func waitEnter(reader *bufio.Reader) {
|
|
fmt.Print("\nPress Enter to return to menu...")
|
|
reader.ReadString('\n')
|
|
}
|
|
|
|
func promptPortList(reader *bufio.Reader, label string) []int {
|
|
input := promptString(reader, label, "")
|
|
if input == "" {
|
|
return nil
|
|
}
|
|
|
|
var ports []int
|
|
for _, part := range strings.Split(input, ",") {
|
|
value, err := strconv.Atoi(strings.TrimSpace(part))
|
|
if err != nil || value <= 0 || value > 65535 {
|
|
fmt.Printf("Ignoring invalid port: %s\n", strings.TrimSpace(part))
|
|
continue
|
|
}
|
|
ports = append(ports, value)
|
|
}
|
|
return ports
|
|
}
|