updated to 0.2.0. New features include VM and container status and controls, snapshot management, Proxmox storage status, recent cluster tasks, and node listings.
This commit is contained in:
1 parent
3ff81eaf02
commit
e35d299418
2 files changed
+253
-182
No files matched your search
+253
-182
@@ -12,11 +12,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "V0.1.4"
|
const version = "V0.2.0"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tmpDir string
|
tmpDir string
|
||||||
numeric = regexp.MustCompile(`^[0-9]+$`)
|
numeric = regexp.MustCompile(`^[0-9]+$`)
|
||||||
|
snapName = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -37,12 +38,14 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
message(
|
||||||
|
"Development software — do not use in production",
|
||||||
|
"This toolkit is still in development and has not been validated for production use.\n\n" +
|
||||||
|
"Use only in a test environment. Actions may affect VMs, containers, storage, or cluster state.",
|
||||||
|
)
|
||||||
mainMenu()
|
mainMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
// whiptail runs a dialog and captures its selected value through file
|
|
||||||
// descriptor 3. The dialog itself uses 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 {
|
||||||
@@ -85,26 +88,11 @@ func message(title, text string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func confirmAction(text string) bool {
|
func confirmAction(text string) bool {
|
||||||
readEnd, writeEnd, err := os.Pipe()
|
cmd := exec.Command("whiptail", "--title", "Confirm", "--yesno", text, "10", "70")
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer readEnd.Close()
|
|
||||||
|
|
||||||
cmd := exec.Command(
|
|
||||||
"whiptail",
|
|
||||||
"--title", "Confirm",
|
|
||||||
"--yesno", text,
|
|
||||||
"10", "70",
|
|
||||||
)
|
|
||||||
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}
|
return cmd.Run() == nil
|
||||||
|
|
||||||
err = cmd.Run()
|
|
||||||
_ = writeEnd.Close()
|
|
||||||
return err == nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func inputValue(title, prompt string) (string, bool) {
|
func inputValue(title, prompt string) (string, bool) {
|
||||||
@@ -153,8 +141,20 @@ func askID(prompt string) (string, bool) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if !numeric.MatchString(value) {
|
if !numeric.MatchString(value) || value == "0" {
|
||||||
message("Invalid ID", "Enter a numeric VM or container ID.")
|
message("Invalid ID", "Enter a non-zero numeric VM or container ID.")
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func askSnapshotName() (string, bool) {
|
||||||
|
value, ok := inputValue("Snapshot name", "Enter a snapshot name (letters, digits, _, ., or -):")
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if value == "" || !snapName.MatchString(value) {
|
||||||
|
message("Invalid snapshot name", "Use only letters, digits, underscores, periods, and hyphens.")
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return value, true
|
return value, true
|
||||||
@@ -173,100 +173,7 @@ func terminalSize(name string, fallback int) int {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func showOutput(title string, name string, args ...string) {
|
func displayText(title, text 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()
|
|
||||||
|
|
||||||
status := 0
|
|
||||||
if runErr != nil {
|
|
||||||
status = 1
|
|
||||||
var exitErr *exec.ExitError
|
|
||||||
if errors.As(runErr, &exitErr) {
|
|
||||||
status = exitErr.ExitCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := os.ReadFile(outputPath)
|
|
||||||
if len(data) == 0 {
|
|
||||||
data = []byte(fmt.Sprintf(
|
|
||||||
"Command completed with exit status %d; no output.\n",
|
|
||||||
status,
|
|
||||||
))
|
|
||||||
} else if status != 0 {
|
|
||||||
data = append(data, []byte(fmt.Sprintf(
|
|
||||||
"\nCommand exited with status %d.\n",
|
|
||||||
status,
|
|
||||||
))...)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
"--scrolltext",
|
|
||||||
"--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"
|
outputPath := tmpDir + "/output.txt"
|
||||||
if err := os.WriteFile(outputPath, []byte(text), 0600); err != nil {
|
if err := os.WriteFile(outputPath, []byte(text), 0600); err != nil {
|
||||||
message("Error", "Could not write command output to the temporary file.")
|
message("Error", "Could not write command output to the temporary file.")
|
||||||
@@ -282,13 +189,66 @@ func showText(title, text string) {
|
|||||||
width = 50
|
width = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
whiptail(
|
whiptail("--scrolltext", "--title", title, "--textbox", outputPath,
|
||||||
"--scrolltext",
|
strconv.Itoa(height), strconv.Itoa(width))
|
||||||
"--title", title,
|
}
|
||||||
"--textbox", outputPath,
|
|
||||||
strconv.Itoa(height),
|
func showOutput(title, name string, args ...string) {
|
||||||
strconv.Itoa(width),
|
output, err := exec.Command(name, args...).CombinedOutput()
|
||||||
)
|
status := 0
|
||||||
|
if err != nil {
|
||||||
|
status = 1
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
status = exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text := string(output)
|
||||||
|
if text == "" {
|
||||||
|
text = fmt.Sprintf("Command completed with exit status %d; no output.\n", status)
|
||||||
|
} else if status != 0 {
|
||||||
|
text += fmt.Sprintf("\nCommand exited with status %d.\n", status)
|
||||||
|
}
|
||||||
|
displayText(title, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showJSON(title, name string, args ...string) {
|
||||||
|
output, err := exec.Command(name, args...).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
text := string(output)
|
||||||
|
if text == "" {
|
||||||
|
text = err.Error()
|
||||||
|
}
|
||||||
|
displayText(title, text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatted bytes.Buffer
|
||||||
|
if err := json.Indent(&formatted, output, "", " "); err != nil {
|
||||||
|
displayText(title, "Could not format JSON:\n"+err.Error()+"\n\n"+string(output))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
displayText(title, formatted.String()+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func showMemory(title, id string) {
|
||||||
|
output, err := exec.Command("qm", "config", id).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
displayText(title, string(output)+"\n"+err.Error())
|
||||||
|
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."}
|
||||||
|
}
|
||||||
|
displayText(title, strings.Join(matches, "\n")+"\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func showResourceNames(title, resourceType string) {
|
func showResourceNames(title, resourceType string) {
|
||||||
@@ -296,11 +256,7 @@ func showResourceNames(title, resourceType string) {
|
|||||||
"pvesh", "get", "/cluster/resources", "--output-format", "json",
|
"pvesh", "get", "/cluster/resources", "--output-format", "json",
|
||||||
).CombinedOutput()
|
).CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
text := string(output)
|
displayText(title, string(output))
|
||||||
if text == "" {
|
|
||||||
text = err.Error()
|
|
||||||
}
|
|
||||||
showText(title, text)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +265,7 @@ func showResourceNames(title, resourceType string) {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(output, &resources); err != nil {
|
if err := json.Unmarshal(output, &resources); err != nil {
|
||||||
showText(title, "Could not parse pvesh output:\n"+err.Error())
|
displayText(title, "Could not parse pvesh output:\n"+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,25 +275,26 @@ func showResourceNames(title, resourceType string) {
|
|||||||
names = append(names, resource.Name)
|
names = append(names, resource.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
showText(title, "No matching resources found.\n")
|
displayText(title, "No matching resources found.\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
displayText(title, strings.Join(names, "\n")+"\n")
|
||||||
showText(title, strings.Join(names, "\n")+"\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func vmMenu() {
|
func vmMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu(
|
choice := menu(
|
||||||
"Virtual Machines (QEMU)",
|
"Virtual Machines (QEMU)", "Choose an action:", 20, 76, 12,
|
||||||
"Choose an action:",
|
|
||||||
18, 72, 10,
|
|
||||||
"list", "List VMs (qm list)",
|
"list", "List VMs (qm list)",
|
||||||
"names", "List VM names",
|
"names", "List VM names",
|
||||||
|
"status", "Show VM status",
|
||||||
"config", "Show VM configuration",
|
"config", "Show VM configuration",
|
||||||
"memory", "Show VM memory setting",
|
"memory", "Show VM memory setting",
|
||||||
|
"start", "Start a VM",
|
||||||
|
"shutdown", "Gracefully shut down a VM",
|
||||||
|
"reboot", "Reboot a VM",
|
||||||
|
"snapshots", "VM snapshots",
|
||||||
"unlock", "Unlock a VM",
|
"unlock", "Unlock a VM",
|
||||||
"back", "Back",
|
"back", "Back",
|
||||||
)
|
)
|
||||||
@@ -351,28 +308,30 @@ func vmMenu() {
|
|||||||
if requireCommand("pvesh") {
|
if requireCommand("pvesh") {
|
||||||
showResourceNames("VM names", "qemu")
|
showResourceNames("VM names", "qemu")
|
||||||
}
|
}
|
||||||
case "config", "memory", "unlock":
|
case "status", "config", "memory", "unlock", "start", "shutdown", "reboot", "snapshots":
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
switch choice {
|
switch choice {
|
||||||
|
case "status":
|
||||||
|
showOutput("VM "+id+" status", "qm", "status", id)
|
||||||
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", id)
|
showMemory("VM "+id+" memory", id)
|
||||||
case "unlock":
|
case "unlock":
|
||||||
if confirmAction(
|
if confirmAction("Unlock VM " + id + "?\n\nOnly do this after confirming no task is still running for this VM.") {
|
||||||
"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)
|
||||||
}
|
}
|
||||||
|
case "start", "shutdown", "reboot":
|
||||||
|
runGuestAction("qm", id, choice, "VM")
|
||||||
|
case "snapshots":
|
||||||
|
snapshotMenu("qm", id, "VM")
|
||||||
}
|
}
|
||||||
case "back", "":
|
case "back", "":
|
||||||
return
|
return
|
||||||
@@ -383,12 +342,15 @@ func vmMenu() {
|
|||||||
func containerMenu() {
|
func containerMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu(
|
choice := menu(
|
||||||
"Containers (LXC)",
|
"Containers (LXC)", "Choose an action:", 18, 76, 10,
|
||||||
"Choose an action:",
|
|
||||||
16, 72, 8,
|
|
||||||
"list", "List containers (pct list)",
|
"list", "List containers (pct list)",
|
||||||
"names", "List container names",
|
"names", "List container names",
|
||||||
|
"status", "Show container status",
|
||||||
"config", "Show container configuration",
|
"config", "Show container configuration",
|
||||||
|
"start", "Start a container",
|
||||||
|
"shutdown", "Gracefully shut down a container",
|
||||||
|
"reboot", "Reboot a container",
|
||||||
|
"snapshots", "Container snapshots",
|
||||||
"unlock", "Unlock a container",
|
"unlock", "Unlock a container",
|
||||||
"back", "Back",
|
"back", "Back",
|
||||||
)
|
)
|
||||||
@@ -402,23 +364,76 @@ func containerMenu() {
|
|||||||
if requireCommand("pvesh") {
|
if requireCommand("pvesh") {
|
||||||
showResourceNames("Container names", "lxc")
|
showResourceNames("Container names", "lxc")
|
||||||
}
|
}
|
||||||
case "config", "unlock":
|
case "status", "config", "unlock", "start", "shutdown", "reboot", "snapshots":
|
||||||
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" {
|
switch choice {
|
||||||
|
case "status":
|
||||||
|
showOutput("Container "+id+" status", "pct", "status", id)
|
||||||
|
case "config":
|
||||||
showOutput("Container "+id+" configuration", "pct", "config", id)
|
showOutput("Container "+id+" configuration", "pct", "config", id)
|
||||||
} else if confirmAction(
|
case "unlock":
|
||||||
"Unlock container " + id + "?\n\n" +
|
if confirmAction("Unlock container " + id + "?\n\nOnly do this after confirming no task is still running for this container.") {
|
||||||
"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 "start", "shutdown", "reboot":
|
||||||
|
runGuestAction("pct", id, choice, "container")
|
||||||
|
case "snapshots":
|
||||||
|
snapshotMenu("pct", id, "container")
|
||||||
|
}
|
||||||
|
case "back", "":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGuestAction(command, id, action, kind string) {
|
||||||
|
prompt := fmt.Sprintf("%s %s %s?", action, kind, id)
|
||||||
|
if action == "shutdown" {
|
||||||
|
prompt += "\n\nThis requests a graceful shutdown."
|
||||||
|
}
|
||||||
|
if !confirmAction(prompt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showOutput(strings.Title(action)+" "+kind+" "+id, command, action, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotMenu(command, id, kind string) {
|
||||||
|
for {
|
||||||
|
choice := menu(
|
||||||
|
kind+" snapshots",
|
||||||
|
"Choose an action:",
|
||||||
|
12, 72, 5,
|
||||||
|
"list", "List snapshots",
|
||||||
|
"create", "Create snapshot",
|
||||||
|
"delete", "Delete snapshot",
|
||||||
|
"back", "Back",
|
||||||
|
)
|
||||||
|
|
||||||
|
switch choice {
|
||||||
|
case "list":
|
||||||
|
showOutput(kind+" "+id+" snapshots", command, "listsnapshot", id)
|
||||||
|
case "create":
|
||||||
|
name, ok := askSnapshotName()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if confirmAction("Create snapshot '" + name + "' for " + kind + " " + id + "?\n\nSnapshots are not a substitute for backups and may consume storage.") {
|
||||||
|
showOutput("Create snapshot "+name, command, "snapshot", id, name)
|
||||||
|
}
|
||||||
|
case "delete":
|
||||||
|
name, ok := askSnapshotName()
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if confirmAction("Permanently delete snapshot '" + name + "' from " + kind + " " + id + "?\n\nThis cannot be undone.") {
|
||||||
|
showOutput("Delete snapshot "+name, command, "delsnapshot", id, name)
|
||||||
}
|
}
|
||||||
case "back", "":
|
case "back", "":
|
||||||
return
|
return
|
||||||
@@ -430,25 +445,18 @@ 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 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(
|
choice := menu(
|
||||||
"Restore",
|
"Restore", "Choose what to restore:", 12, 72, 4,
|
||||||
"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",
|
||||||
@@ -461,11 +469,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -475,11 +479,7 @@ func restoreMenu() {
|
|||||||
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\nConfirm 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,9 +495,8 @@ func restoreMenu() {
|
|||||||
func storageMenu() {
|
func storageMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu(
|
choice := menu(
|
||||||
"Storage / LVM",
|
"Storage", "Choose a report:", 14, 72, 6,
|
||||||
"Choose a read-only report:",
|
"status", "Proxmox storage status (pvesm status)",
|
||||||
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)",
|
||||||
@@ -505,6 +504,10 @@ func storageMenu() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch choice {
|
switch choice {
|
||||||
|
case "status":
|
||||||
|
if requireCommand("pvesm") {
|
||||||
|
showOutput("Proxmox storage status", "pvesm", "status")
|
||||||
|
}
|
||||||
case "pvs":
|
case "pvs":
|
||||||
if requireCommand("pvs") {
|
if requireCommand("pvs") {
|
||||||
showOutput("Physical volumes", "pvs")
|
showOutput("Physical volumes", "pvs")
|
||||||
@@ -523,22 +526,90 @@ func storageMenu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourcesMenu() {
|
||||||
|
for {
|
||||||
|
choice := menu(
|
||||||
|
"Cluster Resources",
|
||||||
|
"Choose a resource type to display as formatted JSON:",
|
||||||
|
16, 76, 8,
|
||||||
|
"vm", "Virtual machines",
|
||||||
|
"storage", "Storage",
|
||||||
|
"node", "Nodes",
|
||||||
|
"sdn", "SDN",
|
||||||
|
"back", "Back",
|
||||||
|
)
|
||||||
|
|
||||||
|
switch choice {
|
||||||
|
case "vm", "storage", "node", "sdn":
|
||||||
|
if requireCommand("pvesh") {
|
||||||
|
showJSON(
|
||||||
|
"Cluster "+choice+" resources",
|
||||||
|
"pvesh",
|
||||||
|
"get", "/cluster/resources",
|
||||||
|
"--type", choice,
|
||||||
|
"--output-format", "json",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case "back", "":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clusterMenu() {
|
||||||
|
for {
|
||||||
|
choice := menu(
|
||||||
|
"Cluster and Node",
|
||||||
|
"Choose a report:",
|
||||||
|
14, 72, 6,
|
||||||
|
"status", "Cluster status (pvecm status)",
|
||||||
|
"nodes", "List nodes",
|
||||||
|
"tasks", "Recent cluster tasks",
|
||||||
|
"back", "Back",
|
||||||
|
)
|
||||||
|
|
||||||
|
switch choice {
|
||||||
|
case "status":
|
||||||
|
if requireCommand("pvecm") {
|
||||||
|
showOutput("Cluster status", "pvecm", "status")
|
||||||
|
}
|
||||||
|
case "nodes":
|
||||||
|
if requireCommand("pvesh") {
|
||||||
|
showJSON("Cluster nodes", "pvesh", "get", "/nodes", "--output-format", "json")
|
||||||
|
}
|
||||||
|
case "tasks":
|
||||||
|
if requireCommand("pvesh") {
|
||||||
|
showJSON(
|
||||||
|
"Recent cluster tasks",
|
||||||
|
"pvesh", "get", "/cluster/tasks",
|
||||||
|
"--limit", "50",
|
||||||
|
"--output-format", "json",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case "back", "":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func mainMenu() {
|
func mainMenu() {
|
||||||
for {
|
for {
|
||||||
choice := menu(
|
choice := menu(
|
||||||
"Proxmox VE Toolkit "+version,
|
"Proxmox VE Toolkit - DEVELOPMENT ONLY - NOT FOR PRODUCTION "+version,
|
||||||
"Select a task:",
|
"Select a task:",
|
||||||
21, 76, 13,
|
22, 76, 14,
|
||||||
"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)",
|
||||||
"restore", "Restore a VM or container",
|
"restore", "Restore a VM or container",
|
||||||
"cluster", "Cluster status (pvecm status)",
|
"cluster", "Cluster and node reports",
|
||||||
"version", "Proxmox version (pveversion -v)",
|
"version", "Proxmox version (pveversion -v)",
|
||||||
"storage", "Storage / LVM reports",
|
"storage", "Storage / LVM reports",
|
||||||
"perf", "Host performance test (pveperf)",
|
"perf", "Host performance test (pveperf)",
|
||||||
"subscription", "Subscription status",
|
"subscription", "Subscription status",
|
||||||
"dab", "Debian Appliance Builder help",
|
"dab", "Debian Appliance Builder help",
|
||||||
|
"resources", "Cluster resources (formatted JSON)",
|
||||||
"quit", "Exit",
|
"quit", "Exit",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -552,9 +623,7 @@ func mainMenu() {
|
|||||||
case "restore":
|
case "restore":
|
||||||
restoreMenu()
|
restoreMenu()
|
||||||
case "cluster":
|
case "cluster":
|
||||||
if requireCommand("pvecm") {
|
clusterMenu()
|
||||||
showOutput("Cluster status", "pvecm", "status")
|
|
||||||
}
|
|
||||||
case "version":
|
case "version":
|
||||||
if requireCommand("pveversion") {
|
if requireCommand("pveversion") {
|
||||||
showOutput("Proxmox VE version", "pveversion", "-v")
|
showOutput("Proxmox VE version", "pveversion", "-v")
|
||||||
@@ -574,6 +643,8 @@ func mainMenu() {
|
|||||||
if requireCommand("dab") {
|
if requireCommand("dab") {
|
||||||
showOutput("Debian Appliance Builder help", "dab", "help")
|
showOutput("Debian Appliance Builder help", "dab", "help")
|
||||||
}
|
}
|
||||||
|
case "resources":
|
||||||
|
resourcesMenu()
|
||||||
case "quit", "":
|
case "quit", "":
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user