Add Windows cloud image tools with firewall support
This commit is contained in:
350
windows-tools/proxmox-windows.go
Normal file
350
windows-tools/proxmox-windows.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func findAvailableVMID(config *WindowsConfig) (int, error) {
|
||||
sshCmd := func(args ...string) *exec.Cmd {
|
||||
fullArgs := []string{
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
fmt.Sprintf("%s@%s", strings.Split(config.ProxmoxUser, "@")[0], config.ProxmoxHost),
|
||||
}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
return exec.Command("ssh", fullArgs...)
|
||||
}
|
||||
|
||||
cmd := sshCmd("qm", "list")
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return 0, fmt.Errorf("failed to list VMs: %w", err)
|
||||
}
|
||||
|
||||
usedIDs := make(map[int]bool)
|
||||
lines := strings.Split(stdout.String(), "\n")
|
||||
for _, line := range lines[1:] {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 {
|
||||
if vmid, err := strconv.Atoi(fields[0]); err == nil {
|
||||
usedIDs[vmid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for vmid := 10000; vmid < 20000; vmid++ {
|
||||
if !usedIDs[vmid] {
|
||||
return vmid, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no available VM ID found in range 10000-19999")
|
||||
}
|
||||
|
||||
func buildNetworkConfig(config *WindowsConfig) string {
|
||||
netConfig := fmt.Sprintf("virtio,bridge=%s", config.Bridge)
|
||||
if config.VlanTag > 0 {
|
||||
netConfig += fmt.Sprintf(",tag=%d", config.VlanTag)
|
||||
}
|
||||
if config.Firewall {
|
||||
netConfig += ",firewall=1"
|
||||
}
|
||||
return netConfig
|
||||
}
|
||||
|
||||
func createProxmoxWindowsVM(config *WindowsConfig) error {
|
||||
fmt.Println("Creating Proxmox Windows template...")
|
||||
|
||||
sshCmd := func(args ...string) *exec.Cmd {
|
||||
fullArgs := []string{
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
fmt.Sprintf("%s@%s", strings.Split(config.ProxmoxUser, "@")[0], config.ProxmoxHost),
|
||||
}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
return exec.Command("ssh", fullArgs...)
|
||||
}
|
||||
|
||||
scpCmd := func(src, dst string) *exec.Cmd {
|
||||
return exec.Command("scp",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
src,
|
||||
fmt.Sprintf("%s@%s:%s", strings.Split(config.ProxmoxUser, "@")[0], config.ProxmoxHost, dst),
|
||||
)
|
||||
}
|
||||
|
||||
imageName := fmt.Sprintf("vm-%d-disk-0.qcow2", config.VMID)
|
||||
remotePath := fmt.Sprintf("/tmp/%s", imageName)
|
||||
|
||||
if _, err := os.Stat(config.ImagePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("Windows image not found at %s", config.ImagePath)
|
||||
}
|
||||
|
||||
fmt.Println("Uploading Windows image to Proxmox host...")
|
||||
maxRetries := 3
|
||||
var uploadErr error
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
fmt.Printf("Retry upload attempt %d/%d...\n", attempt, maxRetries)
|
||||
}
|
||||
|
||||
cmd := scpCmd(config.ImagePath, remotePath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
uploadErr = cmd.Run()
|
||||
|
||||
if uploadErr == nil {
|
||||
fmt.Println("Upload completed!")
|
||||
break
|
||||
}
|
||||
|
||||
if attempt == maxRetries {
|
||||
return fmt.Errorf("failed to upload image after %d attempts: %w", maxRetries, uploadErr)
|
||||
}
|
||||
fmt.Printf("Upload failed: %v\n", uploadErr)
|
||||
}
|
||||
|
||||
fmt.Println("Creating Windows VM and converting to template...")
|
||||
|
||||
createCmd := []string{"qm", "create", fmt.Sprintf("%d", config.VMID),
|
||||
"--name", config.VMName,
|
||||
"--memory", fmt.Sprintf("%d", config.Memory),
|
||||
"--cores", fmt.Sprintf("%d", config.Cores),
|
||||
"--sockets", fmt.Sprintf("%d", config.Sockets),
|
||||
"--cpu", "host",
|
||||
"--ostype", "win11",
|
||||
"--net0", buildNetworkConfig(config),
|
||||
}
|
||||
|
||||
fmt.Printf("Running: %s\n", strings.Join(createCmd, " "))
|
||||
cmd := sshCmd(createCmd...)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create VM: %w", err)
|
||||
}
|
||||
|
||||
importCmd := []string{"qm", "importdisk", fmt.Sprintf("%d", config.VMID), remotePath, config.Storage}
|
||||
fmt.Printf("Running: %s\n", strings.Join(importCmd, " "))
|
||||
|
||||
cmd = sshCmd(importCmd...)
|
||||
var importOut bytes.Buffer
|
||||
cmd.Stdout = &importOut
|
||||
cmd.Stderr = &importOut
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Println(importOut.String())
|
||||
return fmt.Errorf("failed to import disk: %w", err)
|
||||
}
|
||||
|
||||
output := importOut.String()
|
||||
fmt.Println(output)
|
||||
|
||||
var diskPath string
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "successfully imported disk") {
|
||||
start := strings.Index(line, "'")
|
||||
end := strings.LastIndex(line, "'")
|
||||
if start != -1 && end != -1 && end > start {
|
||||
diskPath = line[start+1 : end]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if diskPath == "" {
|
||||
return fmt.Errorf("failed to parse disk path from importdisk output")
|
||||
}
|
||||
|
||||
fmt.Printf("Imported disk path: %s\n", diskPath)
|
||||
|
||||
commands := [][]string{
|
||||
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||
"--scsihw", "virtio-scsi-pci",
|
||||
"--scsi0", diskPath,
|
||||
},
|
||||
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||
"--boot", "order=scsi0",
|
||||
},
|
||||
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||
"--machine", "q35",
|
||||
},
|
||||
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||
"--bios", "ovmf",
|
||||
},
|
||||
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||
"--efidisk0", fmt.Sprintf("%s:1,efitype=4m,pre-enrolled-keys=1", config.Storage),
|
||||
},
|
||||
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||
"--tpmstate0", fmt.Sprintf("%s:1,version=v2.0", config.Storage),
|
||||
},
|
||||
}
|
||||
|
||||
if config.GuestAgent {
|
||||
commands = append(commands, []string{"qm", "set", fmt.Sprintf("%d", config.VMID), "--agent", "enabled=1"})
|
||||
}
|
||||
|
||||
commands = append(commands, [][]string{
|
||||
{"qm", "template", fmt.Sprintf("%d", config.VMID)},
|
||||
{"rm", "-f", remotePath},
|
||||
}...)
|
||||
|
||||
for _, cmdArgs := range commands {
|
||||
fmt.Printf("Running: %s\n", strings.Join(cmdArgs, " "))
|
||||
|
||||
var cmdErr error
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
fmt.Printf("Retry command attempt %d/%d...\n", attempt, maxRetries)
|
||||
}
|
||||
|
||||
cmd := sshCmd(cmdArgs...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
cmdErr = cmd.Run()
|
||||
|
||||
if cmdErr == nil {
|
||||
if stdout.Len() > 0 {
|
||||
fmt.Println(stdout.String())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Println(stdout.String())
|
||||
fmt.Println(stderr.String())
|
||||
|
||||
if attempt == maxRetries {
|
||||
return fmt.Errorf("failed to execute command '%s' after %d attempts: %w", strings.Join(cmdArgs, " "), maxRetries, cmdErr)
|
||||
}
|
||||
|
||||
fmt.Printf("Command failed: %v\n", cmdErr)
|
||||
}
|
||||
}
|
||||
|
||||
if config.Firewall && len(config.FirewallRules) > 0 {
|
||||
if err := configureFirewallRules(config, sshCmd); err != nil {
|
||||
return fmt.Errorf("failed to configure firewall rules: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nWindows Template %s (ID: %d) created successfully!\n", config.VMName, config.VMID)
|
||||
fmt.Printf("You can clone it with: qm clone %d <new-vm-id> --name <new-vm-name>\n", config.VMID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func configureFirewallRules(config *WindowsConfig, sshCmd func(args ...string) *exec.Cmd) error {
|
||||
fmt.Println("Configuring firewall rules...")
|
||||
|
||||
firewallConfig := "[OPTIONS]\nenable: 1\n\n[RULES]\n"
|
||||
|
||||
for _, rule := range config.FirewallRules {
|
||||
ruleLine := fmt.Sprintf("%s %s", strings.ToUpper(rule.Type), strings.ToUpper(rule.Action))
|
||||
|
||||
if rule.Protocol != "" {
|
||||
ruleLine += fmt.Sprintf(" -p %s", rule.Protocol)
|
||||
}
|
||||
if rule.Dport != "" {
|
||||
ruleLine += fmt.Sprintf(" -dport %s", rule.Dport)
|
||||
}
|
||||
if rule.Sport != "" {
|
||||
ruleLine += fmt.Sprintf(" -sport %s", rule.Sport)
|
||||
}
|
||||
if rule.Source != "" {
|
||||
ruleLine += fmt.Sprintf(" -source %s", rule.Source)
|
||||
}
|
||||
if rule.Dest != "" {
|
||||
ruleLine += fmt.Sprintf(" -dest %s", rule.Dest)
|
||||
}
|
||||
if rule.Comment != "" {
|
||||
ruleLine += fmt.Sprintf(" -log nolog # %s", rule.Comment)
|
||||
}
|
||||
|
||||
firewallConfig += ruleLine + "\n"
|
||||
}
|
||||
|
||||
firewallPath := fmt.Sprintf("/etc/pve/firewall/%d.fw", config.VMID)
|
||||
|
||||
createCmd := sshCmd("bash", "-c", fmt.Sprintf("cat > %s << 'EOF'\n%sEOF", firewallPath, firewallConfig))
|
||||
var stdout, stderr bytes.Buffer
|
||||
createCmd.Stdout = &stdout
|
||||
createCmd.Stderr = &stderr
|
||||
|
||||
if err := createCmd.Run(); err != nil {
|
||||
fmt.Println(stdout.String())
|
||||
fmt.Println(stderr.String())
|
||||
return fmt.Errorf("failed to create firewall config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Firewall rules configured: %s\n", firewallPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func listAvailableStorage(config *WindowsConfig) error {
|
||||
fmt.Printf("Detecting available storage on %s...\n", config.ProxmoxHost)
|
||||
|
||||
sshCmd := func(args ...string) *exec.Cmd {
|
||||
fullArgs := []string{
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
fmt.Sprintf("%s@%s", strings.Split(config.ProxmoxUser, "@")[0], config.ProxmoxHost),
|
||||
}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
return exec.Command("ssh", fullArgs...)
|
||||
}
|
||||
|
||||
cmd := sshCmd("pvesm", "status", "--output-format", "json")
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to get storage status: %w", err)
|
||||
}
|
||||
|
||||
var storageList []map[string]interface{}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &storageList); err != nil {
|
||||
return fmt.Errorf("failed to parse storage status: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nAvailable storage:")
|
||||
fmt.Println("================")
|
||||
|
||||
found := false
|
||||
for _, storage := range storageList {
|
||||
nameVal, ok := storage["storage"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := nameVal.(string)
|
||||
|
||||
typeVal, _ := storage["type"]
|
||||
typeStr, _ := typeVal.(string)
|
||||
|
||||
activeVal, _ := storage["active"]
|
||||
activeFloat, _ := activeVal.(float64)
|
||||
|
||||
if activeFloat == 1 {
|
||||
fmt.Printf("- %s (%s) - ACTIVE\n", name, typeStr)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Println("No active storage found!")
|
||||
fmt.Println("\nManual check:")
|
||||
fmt.Println(" ssh root@your-proxmox 'pvesm status'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user