Added versioning - 0.1.2
This commit is contained in:
1 parent
73e60e5797
commit
5550771ab1
3 files changed
+217
-89
No files matched your search
+205
-88
@@ -2,10 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,7 +15,7 @@ var (
|
|||||||
tmpDir string
|
tmpDir string
|
||||||
numeric = regexp.MustCompile(`^[0-9]+$`)
|
numeric = regexp.MustCompile(`^[0-9]+$`)
|
||||||
)
|
)
|
||||||
|
const version = "V0.1.2"
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
fmt.Fprintf(os.Stderr, "Run this program as root, for example: sudo %s\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Run this program as root, for example: sudo %s\n", os.Args[0])
|
||||||
@@ -36,21 +38,8 @@ func main() {
|
|||||||
mainMenu()
|
mainMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// menu displays a whiptail menu and returns the selected tag.
|
// whiptail runs a dialog and captures its selected value through file
|
||||||
// An empty selection means the user cancelled or pressed Esc.
|
// descriptor 3. The dialog itself uses the terminal.
|
||||||
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 {
|
func whiptail(args ...string) string {
|
||||||
readEnd, writeEnd, err := os.Pipe()
|
readEnd, writeEnd, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,7 +55,7 @@ func whiptail(args ...string) string {
|
|||||||
cmd.ExtraFiles = []*os.File{writeEnd}
|
cmd.ExtraFiles = []*os.File{writeEnd}
|
||||||
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
writeEnd.Close()
|
_ = writeEnd.Close()
|
||||||
|
|
||||||
var result bytes.Buffer
|
var result bytes.Buffer
|
||||||
_, _ = result.ReadFrom(readEnd)
|
_, _ = result.ReadFrom(readEnd)
|
||||||
@@ -76,16 +65,22 @@ func whiptail(args ...string) string {
|
|||||||
return strings.TrimSpace(result.String())
|
return strings.TrimSpace(result.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func menu(title, prompt string, height, width, menuHeight int, items ...string) string {
|
||||||
|
args := []string{
|
||||||
|
"--title", title,
|
||||||
|
"--menu", prompt,
|
||||||
|
strconv.Itoa(height),
|
||||||
|
strconv.Itoa(width),
|
||||||
|
strconv.Itoa(menuHeight),
|
||||||
|
}
|
||||||
|
args = append(args, items...)
|
||||||
|
return whiptail(args...)
|
||||||
|
}
|
||||||
|
|
||||||
func message(title, text string) {
|
func message(title, text string) {
|
||||||
whiptail("--title", title, "--msgbox", text, "8", "70")
|
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 {
|
func confirmAction(text string) bool {
|
||||||
readEnd, writeEnd, err := os.Pipe()
|
readEnd, writeEnd, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -94,7 +89,10 @@ func confirmAction(text string) bool {
|
|||||||
defer readEnd.Close()
|
defer readEnd.Close()
|
||||||
|
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"whiptail", "--title", "Confirm", "--yesno", text, "10", "70",
|
"whiptail",
|
||||||
|
"--title", "Confirm",
|
||||||
|
"--yesno", text,
|
||||||
|
"10", "70",
|
||||||
)
|
)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@@ -102,18 +100,10 @@ func confirmAction(text string) bool {
|
|||||||
cmd.ExtraFiles = []*os.File{writeEnd}
|
cmd.ExtraFiles = []*os.File{writeEnd}
|
||||||
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
writeEnd.Close()
|
_ = writeEnd.Close()
|
||||||
_, _ = readEnd.Read(make([]byte, 1))
|
|
||||||
return err == nil
|
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) {
|
func inputValue(title, prompt string) (string, bool) {
|
||||||
readEnd, writeEnd, err := os.Pipe()
|
readEnd, writeEnd, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,15 +111,20 @@ func inputValue(title, prompt string) (string, bool) {
|
|||||||
}
|
}
|
||||||
defer readEnd.Close()
|
defer readEnd.Close()
|
||||||
|
|
||||||
cmd := exec.Command("whiptail", "--output-fd", "3",
|
cmd := exec.Command(
|
||||||
"--title", title, "--inputbox", prompt, "9", "72")
|
"whiptail",
|
||||||
|
"--output-fd", "3",
|
||||||
|
"--title", title,
|
||||||
|
"--inputbox", prompt,
|
||||||
|
"9", "72",
|
||||||
|
)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.ExtraFiles = []*os.File{writeEnd}
|
cmd.ExtraFiles = []*os.File{writeEnd}
|
||||||
|
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
writeEnd.Close()
|
_ = writeEnd.Close()
|
||||||
|
|
||||||
var result bytes.Buffer
|
var result bytes.Buffer
|
||||||
_, _ = result.ReadFrom(readEnd)
|
_, _ = result.ReadFrom(readEnd)
|
||||||
@@ -141,8 +136,10 @@ func inputValue(title, prompt string) (string, bool) {
|
|||||||
|
|
||||||
func requireCommand(name string) bool {
|
func requireCommand(name string) bool {
|
||||||
if _, err := exec.LookPath(name); err != nil {
|
if _, err := exec.LookPath(name); err != nil {
|
||||||
message("Command unavailable",
|
message(
|
||||||
fmt.Sprintf("The command '%s' is not installed or not available on this node.", name))
|
"Command unavailable",
|
||||||
|
fmt.Sprintf("The command '%s' is not installed or not available on this node.", name),
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -160,8 +157,22 @@ func askID(prompt string) (string, bool) {
|
|||||||
return value, true
|
return value, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func terminalSize(name string, fallback int) int {
|
||||||
|
out, err := exec.Command("tput", name).Output()
|
||||||
|
if err != nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := strconv.Atoi(strings.TrimSpace(string(out)))
|
||||||
|
if err != nil || value <= 0 {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
func showOutput(title string, name string, args ...string) {
|
func showOutput(title string, name string, args ...string) {
|
||||||
outputPath := tmpDir + "/output.txt"
|
outputPath := tmpDir + "/output.txt"
|
||||||
|
|
||||||
output, err := os.Create(outputPath)
|
output, err := os.Create(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
message("Error", "Could not create a temporary output file.")
|
message("Error", "Could not create a temporary output file.")
|
||||||
@@ -174,24 +185,113 @@ func showOutput(title string, name string, args ...string) {
|
|||||||
runErr := cmd.Run()
|
runErr := cmd.Run()
|
||||||
_ = output.Close()
|
_ = output.Close()
|
||||||
|
|
||||||
|
status := 0
|
||||||
|
if runErr != nil {
|
||||||
|
status = 1
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(runErr, &exitErr) {
|
||||||
|
status = exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data, _ := os.ReadFile(outputPath)
|
data, _ := os.ReadFile(outputPath)
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
status := 0
|
data = []byte(fmt.Sprintf(
|
||||||
if runErr != nil {
|
"Command completed with exit status %d; no output.\n",
|
||||||
status = 1
|
status,
|
||||||
}
|
))
|
||||||
data = []byte(fmt.Sprintf("Command completed with exit status %d; no output.\n", status))
|
} else if status != 0 {
|
||||||
} else if runErr != nil {
|
data = append(data, []byte(fmt.Sprintf(
|
||||||
data = append(data, []byte("\nCommand exited with an error.\n")...)
|
"\nCommand exited with status %d.\n",
|
||||||
|
status,
|
||||||
|
))...)
|
||||||
}
|
}
|
||||||
_ = os.WriteFile(outputPath, data, 0600)
|
|
||||||
|
|
||||||
whiptail("--title", title, "--textbox", outputPath, "24", "85")
|
if err := os.WriteFile(outputPath, data, 0600); err != nil {
|
||||||
|
message("Error", "Could not write command output to the temporary file.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave room around the dialog and use fallback dimensions if tput fails.
|
||||||
|
height := terminalSize("lines", 24) - 4
|
||||||
|
width := terminalSize("cols", 80) - 4
|
||||||
|
|
||||||
|
// Keep dimensions within whiptail's practical limits.
|
||||||
|
if height < 12 {
|
||||||
|
height = 12
|
||||||
|
}
|
||||||
|
if width < 50 {
|
||||||
|
width = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
whiptail(
|
||||||
|
"--title", title,
|
||||||
|
"--textbox", outputPath,
|
||||||
|
strconv.Itoa(height),
|
||||||
|
strconv.Itoa(width),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showMemory(title, id string) {
|
||||||
|
output, err := exec.Command("qm", "config", id).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
status := 1
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
status = exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
text := string(output)
|
||||||
|
if text != "" {
|
||||||
|
text += "\n"
|
||||||
|
}
|
||||||
|
text += fmt.Sprintf("Command exited with status %d.\n", status)
|
||||||
|
showText(title, text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches []string
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
if strings.HasPrefix(line, "memory:") {
|
||||||
|
matches = append(matches, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
matches = []string{"No memory setting found."}
|
||||||
|
}
|
||||||
|
|
||||||
|
showText(title, strings.Join(matches, "\n")+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func showText(title, text string) {
|
||||||
|
outputPath := tmpDir + "/output.txt"
|
||||||
|
if err := os.WriteFile(outputPath, []byte(text), 0600); err != nil {
|
||||||
|
message("Error", "Could not write command output to the temporary file.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
height := terminalSize("lines", 24) - 4
|
||||||
|
width := terminalSize("cols", 80) - 4
|
||||||
|
if height < 12 {
|
||||||
|
height = 12
|
||||||
|
}
|
||||||
|
if width < 50 {
|
||||||
|
width = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
whiptail(
|
||||||
|
"--title", title,
|
||||||
|
"--textbox", outputPath,
|
||||||
|
strconv.Itoa(height),
|
||||||
|
strconv.Itoa(width),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func vmMenu() {
|
func vmMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu("Virtual Machines (QEMU)", "Choose an action:", 16, 72, 8,
|
choice := menu(
|
||||||
|
"Virtual Machines (QEMU)",
|
||||||
|
"Choose an action:",
|
||||||
|
16, 72, 8,
|
||||||
"list", "List VMs (qm list)",
|
"list", "List VMs (qm list)",
|
||||||
"config", "Show VM configuration",
|
"config", "Show VM configuration",
|
||||||
"memory", "Show VM memory setting",
|
"memory", "Show VM memory setting",
|
||||||
@@ -208,6 +308,7 @@ func vmMenu() {
|
|||||||
if !requireCommand("qm") {
|
if !requireCommand("qm") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
id, ok := askID("Enter the VMID:")
|
id, ok := askID("Enter the VMID:")
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -217,9 +318,12 @@ func vmMenu() {
|
|||||||
case "config":
|
case "config":
|
||||||
showOutput("VM "+id+" configuration", "qm", "config", id)
|
showOutput("VM "+id+" configuration", "qm", "config", id)
|
||||||
case "memory":
|
case "memory":
|
||||||
showMemory("VM "+id+" memory", "qm", "config", id, "^memory:")
|
showMemory("VM "+id+" memory", id)
|
||||||
case "unlock":
|
case "unlock":
|
||||||
if confirmAction("Unlock VM " + id + "?\n\nOnly do this if you have confirmed that no task is still running for this VM.") {
|
if confirmAction(
|
||||||
|
"Unlock VM " + id + "?\n\n" +
|
||||||
|
"Only do this if you have confirmed that no task is still running for this VM.",
|
||||||
|
) {
|
||||||
showOutput("Unlock VM "+id, "qm", "unlock", id)
|
showOutput("Unlock VM "+id, "qm", "unlock", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,31 +333,12 @@ func vmMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
func containerMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu("Containers (LXC)", "Choose an action:", 14, 72, 6,
|
choice := menu(
|
||||||
|
"Containers (LXC)",
|
||||||
|
"Choose an action:",
|
||||||
|
14, 72, 6,
|
||||||
"list", "List containers (pct list)",
|
"list", "List containers (pct list)",
|
||||||
"config", "Show container configuration",
|
"config", "Show container configuration",
|
||||||
"unlock", "Unlock a container",
|
"unlock", "Unlock a container",
|
||||||
@@ -269,13 +354,18 @@ func containerMenu() {
|
|||||||
if !requireCommand("pct") {
|
if !requireCommand("pct") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
id, ok := askID("Enter the container ID:")
|
id, ok := askID("Enter the container ID:")
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if choice == "config" {
|
if choice == "config" {
|
||||||
showOutput("Container "+id+" configuration", "pct", "config", id)
|
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.") {
|
} else if confirmAction(
|
||||||
|
"Unlock container " + id + "?\n\n" +
|
||||||
|
"Only do this if you have confirmed that no task is still running for this container.",
|
||||||
|
) {
|
||||||
showOutput("Unlock container "+id, "pct", "unlock", id)
|
showOutput("Unlock container "+id, "pct", "unlock", id)
|
||||||
}
|
}
|
||||||
case "back", "":
|
case "back", "":
|
||||||
@@ -288,17 +378,25 @@ func backupMenu() {
|
|||||||
if !requireCommand("vzdump") {
|
if !requireCommand("vzdump") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, ok := askID("Enter the VM or container ID to back up:")
|
id, ok := askID("Enter the VM or container ID to back up:")
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if confirmAction("Start a backup for ID " + id + " using vzdump defaults?\n\nReview your node's backup configuration and storage capacity first.") {
|
|
||||||
|
if confirmAction(
|
||||||
|
"Start a backup for ID " + id + " using vzdump defaults?\n\n" +
|
||||||
|
"Review your node's backup configuration and storage capacity first.",
|
||||||
|
) {
|
||||||
showOutput("Backup ID "+id, "vzdump", id)
|
showOutput("Backup ID "+id, "vzdump", id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreMenu() {
|
func restoreMenu() {
|
||||||
choice := menu("Restore", "Choose what to restore:", 12, 72, 4,
|
choice := menu(
|
||||||
|
"Restore",
|
||||||
|
"Choose what to restore:",
|
||||||
|
12, 72, 4,
|
||||||
"vm", "Restore a QEMU VM (qmrestore)",
|
"vm", "Restore a QEMU VM (qmrestore)",
|
||||||
"ct", "Restore a container (pct restore)",
|
"ct", "Restore a container (pct restore)",
|
||||||
"back", "Back",
|
"back", "Back",
|
||||||
@@ -311,17 +409,25 @@ func restoreMenu() {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
archive, ok := inputValue("Backup archive", "Enter the full path to the vzdump archive:")
|
|
||||||
|
archive, ok := inputValue(
|
||||||
|
"Backup archive",
|
||||||
|
"Enter the full path to the vzdump archive:",
|
||||||
|
)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(archive)
|
info, err := os.Stat(archive)
|
||||||
if archive == "" || err != nil || !info.Mode().IsRegular() {
|
if archive == "" || err != nil || !info.Mode().IsRegular() {
|
||||||
message("Archive not found", "That file does not exist or is not a regular file.")
|
message("Archive not found", "That file does not exist or is not a regular file.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !confirmAction("Restore '" + archive + "' as ID " + id + "?\n\nRestoring can create or overwrite resources. Confirm the target ID and archive carefully.") {
|
if !confirmAction(
|
||||||
|
"Restore '" + archive + "' as ID " + id + "?\n\n" +
|
||||||
|
"Restoring can create or overwrite resources. Confirm the target ID and archive carefully.",
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +442,10 @@ func restoreMenu() {
|
|||||||
|
|
||||||
func storageMenu() {
|
func storageMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu("Storage / LVM", "Choose a read-only report:", 14, 72, 6,
|
choice := menu(
|
||||||
|
"Storage / LVM",
|
||||||
|
"Choose a read-only report:",
|
||||||
|
14, 72, 6,
|
||||||
"pvs", "Physical volumes (pvs)",
|
"pvs", "Physical volumes (pvs)",
|
||||||
"vgs", "Volume groups (vgs)",
|
"vgs", "Volume groups (vgs)",
|
||||||
"lvs", "Logical volumes (lvs)",
|
"lvs", "Logical volumes (lvs)",
|
||||||
@@ -344,13 +453,17 @@ func storageMenu() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch choice {
|
switch choice {
|
||||||
case "pvs", "vgs", "lvs":
|
case "pvs":
|
||||||
if requireCommand(choice) {
|
if requireCommand("pvs") {
|
||||||
showOutput(map[string]string{
|
showOutput("Physical volumes", "pvs")
|
||||||
"pvs": "Physical volumes",
|
}
|
||||||
"vgs": "Volume groups",
|
case "vgs":
|
||||||
"lvs": "Logical volumes",
|
if requireCommand("vgs") {
|
||||||
}[choice], choice)
|
showOutput("Volume groups", "vgs")
|
||||||
|
}
|
||||||
|
case "lvs":
|
||||||
|
if requireCommand("lvs") {
|
||||||
|
showOutput("Logical volumes", "lvs")
|
||||||
}
|
}
|
||||||
case "back", "":
|
case "back", "":
|
||||||
return
|
return
|
||||||
@@ -360,7 +473,10 @@ func storageMenu() {
|
|||||||
|
|
||||||
func mainMenu() {
|
func mainMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu("Proxmox VE Toolkit", "Select a task:", 21, 76, 13,
|
choice := menu(
|
||||||
|
"Proxmox VE Toolkit "+version,
|
||||||
|
"Select a task:",
|
||||||
|
21, 76, 13,
|
||||||
"vms", "Virtual machines (QEMU)",
|
"vms", "Virtual machines (QEMU)",
|
||||||
"containers", "Containers (LXC)",
|
"containers", "Containers (LXC)",
|
||||||
"backup", "Back up a VM or container (vzdump)",
|
"backup", "Back up a VM or container (vzdump)",
|
||||||
@@ -394,7 +510,8 @@ func mainMenu() {
|
|||||||
case "storage":
|
case "storage":
|
||||||
storageMenu()
|
storageMenu()
|
||||||
case "perf":
|
case "perf":
|
||||||
if requireCommand("pveperf") && confirmAction("Run pveperf?\n\nThis can cause temporary disk activity.") {
|
if requireCommand("pveperf") &&
|
||||||
|
confirmAction("Run pveperf?\n\nThis can cause temporary disk activity.") {
|
||||||
showOutput("Host performance", "pveperf")
|
showOutput("Host performance", "pveperf")
|
||||||
}
|
}
|
||||||
case "subscription":
|
case "subscription":
|
||||||
|
|||||||
+12
-1
@@ -48,6 +48,7 @@ show_output() {
|
|||||||
shift
|
shift
|
||||||
local output="$TMPDIR_TOOLKIT/output.txt"
|
local output="$TMPDIR_TOOLKIT/output.txt"
|
||||||
local status=0
|
local status=0
|
||||||
|
local lines cols height width
|
||||||
|
|
||||||
"$@" >"$output" 2>&1 || status=$?
|
"$@" >"$output" 2>&1 || status=$?
|
||||||
|
|
||||||
@@ -57,7 +58,17 @@ show_output() {
|
|||||||
printf '\nCommand exited with status %s.\n' "$status" >>"$output"
|
printf '\nCommand exited with status %s.\n' "$status" >>"$output"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
whiptail --title "$title" --textbox "$output" 24 85
|
# Leave room around the dialog; use sensible fallback sizes if tput fails.
|
||||||
|
lines=$(tput lines 2>/dev/null || printf '24')
|
||||||
|
cols=$(tput cols 2>/dev/null || printf '80')
|
||||||
|
height=$((lines - 4))
|
||||||
|
width=$((cols - 4))
|
||||||
|
|
||||||
|
# Keep dimensions within whiptail's practical limits.
|
||||||
|
(( height < 12 )) && height=12
|
||||||
|
(( width < 50 )) && width=50
|
||||||
|
|
||||||
|
whiptail --title "$title" --textbox "$output" "$height" "$width"
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm_action() {
|
confirm_action() {
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user