You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
801 lines
24 KiB
801 lines
24 KiB
package main
|
|
|
|
// License: MIT
|
|
// Copyright 2025 - Robert Strutts
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"flag"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"gopkg.in/yaml.v3"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"text/template"
|
|
"bytes"
|
|
"fmt"
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
// New struct for SPICE connection file
|
|
type SpiceConfig struct {
|
|
Host string
|
|
Port int
|
|
TLSPort int
|
|
Password string
|
|
Title string
|
|
ProxyHost string
|
|
HostSubject string
|
|
Cert string
|
|
}
|
|
|
|
type ProxmoxAuth struct {
|
|
Data struct {
|
|
CSRFPreventionToken string `json:"CSRFPreventionToken"`
|
|
Ticket string `json:"ticket"`
|
|
Username string `json:"username"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
// ProxmoxConfig holds the Proxmox API configuration
|
|
type ProxmoxConfig struct {
|
|
BaseURL string
|
|
APIToken string
|
|
CSRFToken string
|
|
Node string
|
|
VMID string
|
|
}
|
|
|
|
type SpicePermissions struct {
|
|
Data struct {
|
|
Permissions map[string]json.RawMessage `json:"permissions"`
|
|
User string `json:"user"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type VM struct {
|
|
VMID int `json:"vmid"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Node string `json:"node"`
|
|
}
|
|
|
|
type VMResponse struct {
|
|
Data []VM `json:"data"`
|
|
}
|
|
|
|
|
|
type VMList struct {
|
|
Data []struct {
|
|
VMID int `json:"vmid"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Node string `json:"node"`
|
|
CPU float64 `json:"cpu"`
|
|
MaxMem int64 `json:"maxmem"`
|
|
Mem int64 `json:"mem"`
|
|
DiskWrite int64 `json:"diskwrite"`
|
|
DiskRead int64 `json:"diskread"`
|
|
NetIn int64 `json:"netin"`
|
|
NetOut int64 `json:"netout"`
|
|
Template int `json:"template"`
|
|
Uptime int64 `json:"uptime"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type VMSpiceInfo struct {
|
|
Data struct {
|
|
Port int `json:"port"`
|
|
Ticket string `json:"password"`
|
|
Host string `json:"host"`
|
|
TLSPort int `json:"tls-port"`
|
|
Certificate string `json:"ca"`
|
|
ProxyHost string `json:"proxy"`
|
|
HostSubject string `json:"host-subject"`
|
|
Toggle string `json:"toggle-fullscreen"`
|
|
Release string `json:"release-cursor"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type Config struct {
|
|
hostname string
|
|
username string
|
|
password string
|
|
realm string
|
|
vmid string
|
|
node string
|
|
port string
|
|
}
|
|
|
|
type ConfigFile struct {
|
|
Host string `yaml:"host"`
|
|
Port string `yaml:"port"`
|
|
UserName string `yaml:"username"`
|
|
Realm string `yaml:"realm"`
|
|
VMID string `yaml:"vmid"`
|
|
Node string `yaml:"node"`
|
|
}
|
|
|
|
func generateSpiceConfig(spiceInfo *VMSpiceInfo, vmid string) (string, error) {
|
|
tmpl := `[virt-viewer]
|
|
type=spice
|
|
host={{.Host}}
|
|
password={{.Password}}
|
|
tls-port={{.TLSPort}}
|
|
title={{.Title}}
|
|
proxy={{.ProxyHost}}
|
|
host-subject={{.HostSubject}}
|
|
toggle-fullscreen=Shift+F11
|
|
release-cursor=Ctrl+Alt+R
|
|
secure-attention=Ctrl+Alt+Ins
|
|
enable-smartcard=0
|
|
enable-usbredir=1
|
|
fullscreen=0
|
|
ca={{.Cert}}
|
|
delete-this-file=1
|
|
`
|
|
config := SpiceConfig{
|
|
Host: spiceInfo.Data.Host,
|
|
Port: spiceInfo.Data.Port,
|
|
TLSPort: spiceInfo.Data.TLSPort,
|
|
Password: spiceInfo.Data.Ticket,
|
|
Title: fmt.Sprintf("VM %s - SPICE", vmid),
|
|
ProxyHost: spiceInfo.Data.ProxyHost,
|
|
HostSubject: spiceInfo.Data.HostSubject,
|
|
Cert: spiceInfo.Data.Certificate,
|
|
}
|
|
|
|
var configContent bytes.Buffer
|
|
t := template.Must(template.New("spice").Parse(tmpl))
|
|
if err := t.Execute(&configContent, config); err != nil {
|
|
return "", fmt.Errorf("failed to generate config: %v", err)
|
|
}
|
|
|
|
// Create temporary file
|
|
tmpfile, err := ioutil.TempFile("", "spice-*.vv")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp file: %v", err)
|
|
}
|
|
|
|
// Write config to file
|
|
if err := ioutil.WriteFile(tmpfile.Name(), configContent.Bytes(), 0600); err != nil {
|
|
return "", fmt.Errorf("failed to write config: %v", err)
|
|
}
|
|
|
|
return tmpfile.Name(), nil
|
|
}
|
|
|
|
func getAuthTicket(config Config, client *http.Client) (*ProxmoxAuth, error) {
|
|
data := url.Values{}
|
|
data.Set("username", config.username)
|
|
data.Set("password", config.password)
|
|
data.Set("realm", config.realm)
|
|
|
|
apiURL := fmt.Sprintf("https://%s:%s/api2/json/access/ticket", config.hostname, config.port)
|
|
|
|
req, err := http.NewRequest("POST", apiURL, strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create auth request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authentication request failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("auth failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
}
|
|
|
|
var auth ProxmoxAuth
|
|
if err := json.Unmarshal(body, &auth); err != nil {
|
|
return nil, fmt.Errorf("failed to parse auth response: %v", err)
|
|
}
|
|
|
|
if auth.Data.Ticket == "" || auth.Data.CSRFPreventionToken == "" {
|
|
return nil, fmt.Errorf("invalid authentication response: missing ticket or token")
|
|
}
|
|
|
|
return &auth, nil
|
|
}
|
|
|
|
func getSpicePermissions(config Config, auth *ProxmoxAuth, client *http.Client) (*SpicePermissions, error) {
|
|
apiURL := fmt.Sprintf("https://%s:%s/api2/json/access/permissions", config.hostname, config.port)
|
|
|
|
req, err := http.NewRequest("GET", apiURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Cookie", fmt.Sprintf("PVEAuthCookie=%s", auth.Data.Ticket))
|
|
req.Header.Set("CSRFPreventionToken", auth.Data.CSRFPreventionToken)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("permissions request failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("permission check failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
}
|
|
|
|
var perms SpicePermissions
|
|
if err := json.Unmarshal(body, &perms); err != nil {
|
|
return nil, fmt.Errorf("failed to parse permissions response: %v", err)
|
|
}
|
|
|
|
return &perms, nil
|
|
}
|
|
|
|
func getVMList(config Config, auth *ProxmoxAuth, client *http.Client) (*VMList, error) {
|
|
apiURL := fmt.Sprintf("https://%s:%s/api2/json/cluster/resources", config.hostname, config.port)
|
|
|
|
req, err := http.NewRequest("GET", apiURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
|
|
// Add query parameters for VMs only
|
|
q := req.URL.Query()
|
|
q.Add("type", "vm")
|
|
req.URL.RawQuery = q.Encode()
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Cookie", fmt.Sprintf("PVEAuthCookie=%s", auth.Data.Ticket))
|
|
req.Header.Set("CSRFPreventionToken", auth.Data.CSRFPreventionToken)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("VM list request failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("VM list request failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
}
|
|
|
|
var vms VMList
|
|
if err := json.Unmarshal(body, &vms); err != nil {
|
|
return nil, fmt.Errorf("failed to parse VM list response: %v", err)
|
|
}
|
|
|
|
return &vms, nil
|
|
}
|
|
|
|
func getVMSpiceInfo(config Config, auth *ProxmoxAuth, client *http.Client) (*VMSpiceInfo, error) {
|
|
if config.vmid == "" || config.node == "" {
|
|
return nil, fmt.Errorf("VMID and node are required for SPICE info")
|
|
}
|
|
|
|
apiURL := fmt.Sprintf("https://%s:%s/api2/json/nodes/%s/qemu/%s/spiceproxy",
|
|
config.hostname, config.port, config.node, config.vmid)
|
|
|
|
req, err := http.NewRequest("POST", apiURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Cookie", fmt.Sprintf("PVEAuthCookie=%s", auth.Data.Ticket))
|
|
req.Header.Set("CSRFPreventionToken", auth.Data.CSRFPreventionToken)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("SPICE info request failed: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("SPICE info request failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
}
|
|
|
|
// fmt.Printf("\n%s\n\n", string(body))
|
|
|
|
var spiceInfo VMSpiceInfo
|
|
if err := json.Unmarshal(body, &spiceInfo); err != nil {
|
|
return nil, fmt.Errorf("failed to parse SPICE info response: %v", err)
|
|
}
|
|
|
|
// Set default values for optional fields if not provided
|
|
if spiceInfo.Data.Toggle == "" {
|
|
spiceInfo.Data.Toggle = "shift+f11"
|
|
}
|
|
if spiceInfo.Data.Release == "" {
|
|
spiceInfo.Data.Release = "shift+f12"
|
|
}
|
|
|
|
// Extract the host and port from the URL part
|
|
colonIndex := strings.LastIndex(spiceInfo.Data.ProxyHost, ":")
|
|
if colonIndex == -1 {
|
|
host := fmt.Sprintf("http://%s:3128", config.hostname)
|
|
spiceInfo.Data.ProxyHost = host
|
|
} else {
|
|
// Get the port number starting from the last ':'
|
|
port := spiceInfo.Data.ProxyHost[colonIndex:]
|
|
host := fmt.Sprintf("http://%s%s", config.hostname, port)
|
|
spiceInfo.Data.ProxyHost = host
|
|
}
|
|
|
|
return &spiceInfo, nil
|
|
}
|
|
|
|
|
|
func launchSpiceViewer(configFile string) error {
|
|
// Check if remote-viewer is installed
|
|
_, err := exec.LookPath("remote-viewer")
|
|
if err != nil {
|
|
return fmt.Errorf("remote-viewer not found. Please install virt-viewer package")
|
|
}
|
|
|
|
// Launch remote-viewer with the config file
|
|
cmd := exec.Command("remote-viewer", configFile) // cat
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start remote-viewer: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func doConnect(config Config, auth *ProxmoxAuth, client *http.Client, vmid string, node string, myVMWindow fyne.Window) error {
|
|
//dialog.ShowInformation("Connect", fmt.Sprintf("Connecting to VM %s on node %s...", vmid, node), myVMWindow)
|
|
|
|
// If VMID and node are provided, get SPICE connection info
|
|
|
|
spiceInfo, err := getVMSpiceInfo(config, auth, client)
|
|
if err != nil {
|
|
fmt.Errorf("failed to get SPICE info: %v", err)
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("\nSPICE Connection Info for VM %s:\n", vmid)
|
|
fmt.Printf("Host: %s\nTLS Port: %d\n",
|
|
spiceInfo.Data.Host, spiceInfo.Data.TLSPort)
|
|
|
|
fmt.Println("\nLaunching SPICE viewer...")
|
|
configFile, err := generateSpiceConfig(spiceInfo, vmid)
|
|
if err != nil {
|
|
fmt.Errorf("failed to generate SPICE config: %v", err)
|
|
return err
|
|
}
|
|
|
|
if err := launchSpiceViewer(configFile); err != nil {
|
|
fmt.Errorf("failed to launch SPICE viewer: %v", err)
|
|
return err
|
|
}
|
|
|
|
fmt.Println("SPICE viewer launched successfully!")
|
|
|
|
return nil
|
|
}
|
|
|
|
|
|
func fetchVMList(config Config, auth *ProxmoxAuth, client *http.Client) ([]map[string]string, error) {
|
|
// Get VM list and status
|
|
vms, err := getVMList(config, auth, client)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get VM list: %v", err)
|
|
}
|
|
|
|
// Process VM data
|
|
var vmList []map[string]string
|
|
for _, vm := range vms.Data {
|
|
vmList = append(vmList, map[string]string{
|
|
"vmid": fmt.Sprintf("%d", vm.VMID),
|
|
"name": vm.Name,
|
|
"status": vm.Status,
|
|
"node": vm.Node,
|
|
})
|
|
}
|
|
|
|
return vmList, nil
|
|
}
|
|
|
|
// sendPostRequest sends a POST request to the Proxmox API
|
|
func sendPostRequest(url, apiToken string, CSRFToken string, client *http.Client) error {
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(nil))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Set headers for API authentication
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Cookie", fmt.Sprintf("PVEAuthCookie=%s", apiToken))
|
|
req.Header.Set("CSRFPreventionToken", CSRFToken)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
return fmt.Errorf("API error: %s, status: %d", body, resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PowerOn powers on a VM
|
|
func PowerOn(config ProxmoxConfig, client *http.Client) error {
|
|
url := fmt.Sprintf("%s/nodes/%s/qemu/%s/status/start", config.BaseURL, config.Node, config.VMID)
|
|
return sendPostRequest(url, config.APIToken, config.CSRFToken, client)
|
|
}
|
|
|
|
// PowerOff powers off a VM
|
|
func PowerOff(config ProxmoxConfig, client *http.Client) error {
|
|
url := fmt.Sprintf("%s/nodes/%s/qemu/%s/status/stop", config.BaseURL, config.Node, config.VMID)
|
|
return sendPostRequest(url, config.APIToken, config.CSRFToken, client)
|
|
}
|
|
|
|
func connectToSpice(myVMWindow fyne.Window, config Config, shouldConnect bool) error {
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
client := &http.Client{Transport: tr}
|
|
|
|
// Get authentication ticket
|
|
auth, err := getAuthTicket(config, client)
|
|
if err != nil {
|
|
return fmt.Errorf("authentication failed: %v", err)
|
|
}
|
|
|
|
// Get SPICE permissions
|
|
perms, err := getSpicePermissions(config, auth, client)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get permissions: %v", err)
|
|
}
|
|
|
|
fmt.Println("\nSPICE-related permissions:")
|
|
for path, permissions := range perms.Data.Permissions {
|
|
if strings.Contains(strings.ToLower(path), "spice") {
|
|
fmt.Printf("Path: %s\nPermissions: %v\n\n", path, permissions)
|
|
}
|
|
}
|
|
|
|
// VM data
|
|
vmList, err := fetchVMList(config, auth, client)
|
|
if err != nil {
|
|
return fmt.Errorf("VM list error: %v", err)
|
|
}
|
|
|
|
// Variable to track selected VM
|
|
var selectedVM map[string]string
|
|
|
|
// Declare listWidget to use it inside the initialization
|
|
var listWidget *widget.List
|
|
|
|
// List widget to display VMs
|
|
listWidget = widget.NewList(
|
|
func() int {
|
|
return len(vmList)
|
|
},
|
|
func() fyne.CanvasObject {
|
|
// Create a horizontal box with a label and a button
|
|
label := widget.NewLabel("")
|
|
button := widget.NewButton("", nil)
|
|
return container.NewHBox(label, button)
|
|
},
|
|
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
vm := vmList[id]
|
|
label := item.(*fyne.Container).Objects[0].(*widget.Label)
|
|
button := item.(*fyne.Container).Objects[1].(*widget.Button)
|
|
|
|
// Set the label text
|
|
label.SetText(fmt.Sprintf("VMID: %s | Name: %s | Node: %s | Status: %s", vm["vmid"], vm["name"], vm["node"], vm["status"]))
|
|
|
|
// Set the button label and action
|
|
if strings.TrimSpace(vm["status"]) == "running" {
|
|
button.SetText("Power Off")
|
|
} else {
|
|
button.SetText("Power On")
|
|
}
|
|
|
|
button.OnTapped = func() {
|
|
configAPI := ProxmoxConfig{
|
|
BaseURL: fmt.Sprintf("https://%s:%s/api2/json", config.hostname, config.port),
|
|
APIToken: auth.Data.Ticket,
|
|
CSRFToken: auth.Data.CSRFPreventionToken,
|
|
Node: vm["node"],
|
|
VMID: vm["vmid"],
|
|
}
|
|
|
|
// Toggle the VM's status
|
|
if vm["status"] == "Running" {
|
|
// Power Off the VM
|
|
err = PowerOff(configAPI, client)
|
|
if err != nil {
|
|
fmt.Printf("Failed to power off VM: %v\n", err)
|
|
} else {
|
|
fmt.Println("VM powered off successfully!")
|
|
vmList[id]["status"] = "Stopped"
|
|
// Refresh the list to reflect changes
|
|
listWidget.Refresh()
|
|
}
|
|
} else {
|
|
// Power On the VM
|
|
err := PowerOn(configAPI, client)
|
|
if err != nil {
|
|
fmt.Printf("Failed to power on VM: %v\n", err)
|
|
} else {
|
|
fmt.Println("VM powered on successfully!")
|
|
vmList[id]["status"] = "Running"
|
|
// Refresh the list to reflect changes
|
|
listWidget.Refresh()
|
|
}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
// Handle selection
|
|
listWidget.OnSelected = func(id widget.ListItemID) {
|
|
selectedVM = vmList[id]
|
|
}
|
|
|
|
// Connect button functionality
|
|
connectButton := widget.NewButton("Connect", func() {
|
|
if selectedVM == nil {
|
|
if config.vmid != "" && config.node != "" {
|
|
vmid := config.vmid
|
|
node := config.node
|
|
err := doConnect(config, auth, client, vmid, node, myVMWindow)
|
|
if err != nil {
|
|
dialog.ShowError(err, myVMWindow)
|
|
return
|
|
}
|
|
} else {
|
|
dialog.ShowInformation("No Selection", "Please select a VM from the list.", myVMWindow)
|
|
return
|
|
}
|
|
} else {
|
|
vmid, node := selectedVM["vmid"], selectedVM["node"]
|
|
config.vmid = vmid
|
|
config.node = node
|
|
err := doConnect(config, auth, client, vmid, node, myVMWindow)
|
|
if err != nil {
|
|
dialog.ShowError(err, myVMWindow)
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
// Refresh button functionality
|
|
refreshButton := widget.NewButton("Refresh", func() {
|
|
newVmList, err := fetchVMList(config, auth, client)
|
|
if err != nil {
|
|
dialog.ShowError(err, myVMWindow)
|
|
return
|
|
}
|
|
vmList = newVmList
|
|
selectedVM = nil // Clear selection on refresh
|
|
listWidget.Refresh()
|
|
})
|
|
|
|
// Layout with full-screen list
|
|
buttons := container.NewHBox(refreshButton, connectButton)
|
|
content := container.NewBorder(buttons, nil, nil, nil, container.NewMax(listWidget)) // Full-screen list
|
|
myVMWindow.SetContent(content)
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
cli_configfilename := flag.String("config", "", "Config YAML file")
|
|
cli_hostname := flag.String("host", "", "Proxmox host address")
|
|
cli_port := flag.String("port", "8006", "Proxmox Port #")
|
|
cli_username := flag.String("user", "", "Username")
|
|
cli_realm := flag.String("realm", "pve", "Authentication realm (default: pve)")
|
|
cli_vmid := flag.String("vmid", "", "VM ID (future use)")
|
|
cli_node := flag.String("node", "", "Node name (optional)")
|
|
cli_connect := flag.Bool("connect", true, "Launch SPICE connection (requires remote-viewer)")
|
|
flag.Parse()
|
|
|
|
label_hostname := ""
|
|
label_port := ""
|
|
label_username := ""
|
|
label_realm := ""
|
|
label_vmid := ""
|
|
label_node := ""
|
|
|
|
if *cli_configfilename != "" {
|
|
// Read the config file
|
|
data, err := os.ReadFile(*cli_configfilename)
|
|
if err != nil {
|
|
fmt.Printf("Error reading config file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
// Use ConfigFile struct to hold our configuration in myconfig
|
|
var myconfig ConfigFile
|
|
|
|
// Unmarshal the YAML data into our struct
|
|
err = yaml.Unmarshal(data, &myconfig)
|
|
if err != nil {
|
|
fmt.Printf("Error parsing YAML: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
label_hostname = myconfig.Host
|
|
if myconfig.Port != "" {
|
|
label_port = myconfig.Port
|
|
} else {
|
|
label_port = *cli_port
|
|
}
|
|
label_username = myconfig.UserName
|
|
if myconfig.Realm != "" {
|
|
label_realm = myconfig.Realm
|
|
} else {
|
|
label_realm = *cli_realm
|
|
}
|
|
label_vmid = myconfig.VMID
|
|
label_node = myconfig.Node
|
|
} else {
|
|
label_hostname = *cli_hostname
|
|
label_port = *cli_port
|
|
label_username = *cli_username
|
|
label_realm = *cli_realm
|
|
label_vmid = *cli_vmid
|
|
label_node = *cli_node
|
|
}
|
|
|
|
myApp := app.New()
|
|
myWindow := myApp.NewWindow("Proxmox SPICE Viewer")
|
|
|
|
// Create entry fields for host IP, username, and password
|
|
hostEntry := widget.NewEntry()
|
|
hostEntry.Text = label_hostname // Set default value
|
|
hostEntry.SetPlaceHolder("Enter Host IP")
|
|
|
|
portEntry := widget.NewEntry()
|
|
portEntry.Text = label_port // Set default value
|
|
portEntry.SetPlaceHolder("Enter Port #")
|
|
|
|
// Create username and password entry fields
|
|
usernameEntry := widget.NewEntry()
|
|
usernameEntry.Text = label_username
|
|
usernameEntry.SetPlaceHolder("Enter Username")
|
|
|
|
passwordEntry := widget.NewPasswordEntry()
|
|
passwordEntry.SetPlaceHolder("Enter Password")
|
|
|
|
// Create a dropdown for realm selection
|
|
realmOptions := []string{"pve", "pam"}
|
|
realmSelect := widget.NewSelect(realmOptions, nil)
|
|
realmSelect.SetSelected(label_realm)
|
|
|
|
// Create a login button
|
|
loginButton := widget.NewButton("Login", func() {
|
|
hostname := hostEntry.Text
|
|
port := portEntry.Text
|
|
username := usernameEntry.Text
|
|
password := passwordEntry.Text
|
|
realm := realmSelect.Selected
|
|
|
|
// Validate hostname
|
|
if hostname == "" {
|
|
dialog.ShowError(fmt.Errorf("host IP cannot be empty"), myWindow)
|
|
return
|
|
}
|
|
|
|
if port == "" {
|
|
dialog.ShowError(fmt.Errorf("host Port cannot be empty"), myWindow)
|
|
return
|
|
}
|
|
|
|
// Validate username
|
|
if username == "" {
|
|
dialog.ShowError(fmt.Errorf("username cannot be empty"), myWindow)
|
|
return
|
|
}
|
|
|
|
// Validate password
|
|
if password == "" {
|
|
dialog.ShowError(fmt.Errorf("password cannot be empty"), myWindow)
|
|
return
|
|
}
|
|
|
|
if realm == "" {
|
|
dialog.ShowError(fmt.Errorf("Realm cannot be empty"), myWindow)
|
|
return
|
|
}
|
|
|
|
// If all validations pass, proceed with login
|
|
//dialog.ShowInformation("Login Attempt",
|
|
// fmt.Sprintf("Attempting to login with:\nHost: %s\nUsername: %s\nRealm: %s",
|
|
// hostname, username, realm),
|
|
// myWindow)
|
|
})
|
|
|
|
// Create connect button
|
|
connectButton := widget.NewButton("Connect to SPICE", func() {
|
|
hostname := hostEntry.Text
|
|
port := portEntry.Text
|
|
username := usernameEntry.Text
|
|
password := passwordEntry.Text
|
|
realm := realmSelect.Selected
|
|
|
|
if hostname == "" {
|
|
dialog.ShowError(fmt.Errorf("Please login first"), myWindow)
|
|
return
|
|
}
|
|
//dialog.ShowInformation("Connecting", fmt.Sprintf("Attempting to connect to host: %s", hostEntry.Text), myWindow)
|
|
config := Config{
|
|
hostname: hostname,
|
|
port: port,
|
|
username: username,
|
|
password: password,
|
|
realm: realm,
|
|
vmid: label_vmid,
|
|
node: label_node,
|
|
}
|
|
|
|
myVMWindow := myApp.NewWindow("VM Manager")
|
|
myVMWindow.Resize(fyne.NewSize(900, 900))
|
|
|
|
if err := connectToSpice(myVMWindow, config, *cli_connect); err != nil {
|
|
fmt.Printf("Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
myVMWindow.Show()
|
|
return
|
|
})
|
|
|
|
// Create a container and set the layout
|
|
content := container.NewVBox(
|
|
widget.NewLabel("Proxmox SPICE Viewer Login"),
|
|
hostEntry,
|
|
portEntry,
|
|
usernameEntry,
|
|
passwordEntry,
|
|
widget.NewLabel("Select Realm:"),
|
|
realmSelect,
|
|
loginButton,
|
|
connectButton,
|
|
)
|
|
|
|
// Set the content of the window and display it
|
|
myWindow.SetContent(content)
|
|
myWindow.Resize(fyne.NewSize(400, 300))
|
|
myWindow.ShowAndRun()
|
|
}
|
|
|