Added go build file
This commit is contained in:
1 parent
6f44deff64
commit
aafd7050cf
1 file changed
+412
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user