From aafd7050cf2c5e76297c4310ae32896163a52dab Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 29 Oct 2026 21:55:16 +0000 Subject: [PATCH] Added go build file --- main.go | 412 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 main.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..e9b026a --- /dev/null +++ b/main.go @@ -0,0 +1,412 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "regexp" + "strings" +) + +var ( + tmpDir string + numeric = regexp.MustCompile(`^[0-9]+$`) +) + +func main() { + if os.Geteuid() != 0 { + fmt.Fprintf(os.Stderr, "Run this program as root, for example: sudo %s\n", os.Args[0]) + os.Exit(1) + } + + if _, err := exec.LookPath("whiptail"); err != nil { + fmt.Fprintln(os.Stderr, "whiptail is required. Install it with: apt install whiptail") + os.Exit(1) + } + + var err error + tmpDir, err = os.MkdirTemp("", "proxmox-toolkit-*") + if err != nil { + fmt.Fprintf(os.Stderr, "Could not create temporary directory: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(tmpDir) + + mainMenu() +} + +// menu displays a whiptail menu and returns the selected tag. +// An empty selection means the user cancelled or pressed Esc. +func menu(title, prompt string, height, width, menuHeight int, items ...string) string { + args := []string{ + "--title", title, + "--menu", prompt, + fmt.Sprint(height), fmt.Sprint(width), fmt.Sprint(menuHeight), + } + args = append(args, items...) + + return whiptail(args...) +} + +// dialog runs whiptail and captures its result through file descriptor 3. +// The menu itself continues to use the terminal. +func whiptail(args ...string) string { + readEnd, writeEnd, err := os.Pipe() + if err != nil { + return "" + } + defer readEnd.Close() + + args = append([]string{"--output-fd", "3"}, args...) + cmd := exec.Command("whiptail", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{writeEnd} + + err = cmd.Run() + writeEnd.Close() + + var result bytes.Buffer + _, _ = result.ReadFrom(readEnd) + if err != nil { + return "" + } + return strings.TrimSpace(result.String()) +} + +func message(title, text string) { + whiptail("--title", title, "--msgbox", text, "8", "70") +} + +func confirm(text string) bool { + return whiptail("--title", "Confirm", "--yesno", text, "10", "70") == "" +} + +// Yes/no dialogs also return an empty string on success, so use this +// separate helper to distinguish Yes from No or Esc by the process status. +func confirmAction(text string) bool { + readEnd, writeEnd, err := os.Pipe() + if err != nil { + return false + } + defer readEnd.Close() + + cmd := exec.Command( + "whiptail", "--title", "Confirm", "--yesno", text, "10", "70", + ) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{writeEnd} + + err = cmd.Run() + writeEnd.Close() + _, _ = readEnd.Read(make([]byte, 1)) + return err == nil +} + +func input(title, prompt string) (string, bool) { + value := whiptail("--title", title, "--inputbox", prompt, "9", "72") + // An empty value is valid for some prompts, so cancellation is + // determined by whiptail itself in inputValue below when needed. + return value, true +} + +func inputValue(title, prompt string) (string, bool) { + readEnd, writeEnd, err := os.Pipe() + if err != nil { + return "", false + } + defer readEnd.Close() + + cmd := exec.Command("whiptail", "--output-fd", "3", + "--title", title, "--inputbox", prompt, "9", "72") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = []*os.File{writeEnd} + + err = cmd.Run() + writeEnd.Close() + + var result bytes.Buffer + _, _ = result.ReadFrom(readEnd) + if err != nil { + return "", false + } + return strings.TrimSpace(result.String()), true +} + +func requireCommand(name string) bool { + if _, err := exec.LookPath(name); err != nil { + message("Command unavailable", + fmt.Sprintf("The command '%s' is not installed or not available on this node.", name)) + return false + } + return true +} + +func askID(prompt string) (string, bool) { + value, ok := inputValue("Enter ID", prompt) + if !ok { + return "", false + } + if !numeric.MatchString(value) { + message("Invalid ID", "Enter a numeric VM or container ID.") + return "", false + } + return value, true +} + +func showOutput(title string, name string, args ...string) { + outputPath := tmpDir + "/output.txt" + output, err := os.Create(outputPath) + if err != nil { + message("Error", "Could not create a temporary output file.") + return + } + + cmd := exec.Command(name, args...) + cmd.Stdout = output + cmd.Stderr = output + runErr := cmd.Run() + _ = output.Close() + + data, _ := os.ReadFile(outputPath) + if len(data) == 0 { + status := 0 + if runErr != nil { + status = 1 + } + data = []byte(fmt.Sprintf("Command completed with exit status %d; no output.\n", status)) + } else if runErr != nil { + data = append(data, []byte("\nCommand exited with an error.\n")...) + } + _ = os.WriteFile(outputPath, data, 0600) + + whiptail("--title", title, "--textbox", outputPath, "24", "85") +} + +func vmMenu() { + for { + choice := menu("Virtual Machines (QEMU)", "Choose an action:", 16, 72, 8, + "list", "List VMs (qm list)", + "config", "Show VM configuration", + "memory", "Show VM memory setting", + "unlock", "Unlock a VM", + "back", "Back", + ) + + switch choice { + case "list": + if requireCommand("qm") { + showOutput("VM list", "qm", "list") + } + case "config", "memory", "unlock": + if !requireCommand("qm") { + continue + } + id, ok := askID("Enter the VMID:") + if !ok { + continue + } + + switch choice { + case "config": + showOutput("VM "+id+" configuration", "qm", "config", id) + case "memory": + showMemory("VM "+id+" memory", "qm", "config", id, "^memory:") + case "unlock": + if confirmAction("Unlock VM " + id + "?\n\nOnly do this if you have confirmed that no task is still running for this VM.") { + showOutput("Unlock VM "+id, "qm", "unlock", id) + } + } + case "back", "": + return + } + } +} + +func showMemory(title, command string, args ...string) { + output, err := exec.Command(command, args[:len(args)-1]...).CombinedOutput() + if err != nil { + showOutput(title, command, args[:len(args)-1]...) + return + } + + var matches []string + for _, line := range strings.Split(string(output), "\n") { + if matched, _ := regexp.MatchString(args[len(args)-1], line); matched { + matches = append(matches, line) + } + } + if len(matches) == 0 { + matches = []string{"No memory setting found."} + } + + path := tmpDir + "/output.txt" + _ = os.WriteFile(path, []byte(strings.Join(matches, "\n")+"\n"), 0600) + whiptail("--title", title, "--textbox", path, "24", "85") +} + +func containerMenu() { + for { + choice := menu("Containers (LXC)", "Choose an action:", 14, 72, 6, + "list", "List containers (pct list)", + "config", "Show container configuration", + "unlock", "Unlock a container", + "back", "Back", + ) + + switch choice { + case "list": + if requireCommand("pct") { + showOutput("Container list", "pct", "list") + } + case "config", "unlock": + if !requireCommand("pct") { + continue + } + id, ok := askID("Enter the container ID:") + if !ok { + continue + } + if choice == "config" { + showOutput("Container "+id+" configuration", "pct", "config", id) + } else if confirmAction("Unlock container " + id + "?\n\nOnly do this if you have confirmed that no task is still running for this container.") { + showOutput("Unlock container "+id, "pct", "unlock", id) + } + case "back", "": + return + } + } +} + +func backupMenu() { + if !requireCommand("vzdump") { + return + } + id, ok := askID("Enter the VM or container ID to back up:") + if !ok { + return + } + if confirmAction("Start a backup for ID " + id + " using vzdump defaults?\n\nReview your node's backup configuration and storage capacity first.") { + showOutput("Backup ID "+id, "vzdump", id) + } +} + +func restoreMenu() { + choice := menu("Restore", "Choose what to restore:", 12, 72, 4, + "vm", "Restore a QEMU VM (qmrestore)", + "ct", "Restore a container (pct restore)", + "back", "Back", + ) + if choice != "vm" && choice != "ct" { + return + } + + id, ok := askID("Enter the new VMID or container ID:") + if !ok { + return + } + archive, ok := inputValue("Backup archive", "Enter the full path to the vzdump archive:") + if !ok { + return + } + info, err := os.Stat(archive) + if archive == "" || err != nil || !info.Mode().IsRegular() { + message("Archive not found", "That file does not exist or is not a regular file.") + return + } + + if !confirmAction("Restore '" + archive + "' as ID " + id + "?\n\nRestoring can create or overwrite resources. Confirm the target ID and archive carefully.") { + return + } + + if choice == "vm" { + if requireCommand("qmrestore") { + showOutput("Restore VM "+id, "qmrestore", archive, id) + } + } else if requireCommand("pct") { + showOutput("Restore container "+id, "pct", "restore", id, archive) + } +} + +func storageMenu() { + for { + choice := menu("Storage / LVM", "Choose a read-only report:", 14, 72, 6, + "pvs", "Physical volumes (pvs)", + "vgs", "Volume groups (vgs)", + "lvs", "Logical volumes (lvs)", + "back", "Back", + ) + + switch choice { + case "pvs", "vgs", "lvs": + if requireCommand(choice) { + showOutput(map[string]string{ + "pvs": "Physical volumes", + "vgs": "Volume groups", + "lvs": "Logical volumes", + }[choice], choice) + } + case "back", "": + return + } + } +} + +func mainMenu() { + for { + choice := menu("Proxmox VE Toolkit", "Select a task:", 21, 76, 13, + "vms", "Virtual machines (QEMU)", + "containers", "Containers (LXC)", + "backup", "Back up a VM or container (vzdump)", + "restore", "Restore a VM or container", + "cluster", "Cluster status (pvecm status)", + "version", "Proxmox version (pveversion -v)", + "storage", "Storage / LVM reports", + "perf", "Host performance test (pveperf)", + "subscription", "Subscription status", + "dab", "Debian Appliance Builder help", + "quit", "Exit", + ) + + switch choice { + case "vms": + vmMenu() + case "containers": + containerMenu() + case "backup": + backupMenu() + case "restore": + restoreMenu() + case "cluster": + if requireCommand("pvecm") { + showOutput("Cluster status", "pvecm", "status") + } + case "version": + if requireCommand("pveversion") { + showOutput("Proxmox VE version", "pveversion", "-v") + } + case "storage": + storageMenu() + case "perf": + if requireCommand("pveperf") && confirmAction("Run pveperf?\n\nThis can cause temporary disk activity.") { + showOutput("Host performance", "pveperf") + } + case "subscription": + if requireCommand("pvesubscription") { + showOutput("Subscription status", "pvesubscription", "get") + } + case "dab": + if requireCommand("dab") { + showOutput("Debian Appliance Builder help", "dab", "help") + } + case "quit", "": + return + } + } +}