development #3

Merged
othman.suseno merged 5 commits from development into main 2025-11-18 08:46:29 +00:00
24 changed files with 1108 additions and 11 deletions

View File

@@ -13,6 +13,26 @@ Tool untuk membuat **template** di Proxmox menggunakan cloud image (Ubuntu, Debi
- **Firewall configuration** (enable/disable + custom rules) - **Firewall configuration** (enable/disable + custom rules)
- **Batch mode** untuk create multiple templates sekaligus - **Batch mode** untuk create multiple templates sekaligus
## Tools
### 1. Linux Cloud Images (Main Tool)
Tool utama untuk Linux cloud images (Ubuntu, Debian, CentOS, Rocky, dll).
📁 **Location**: Root directory
📖 **Docs**: [README.md](README.md) (this file)
### 2. Windows Cloud Images
Tool terpisah untuk Windows cloud images (Windows Server, Windows 11).
📁 **Location**: `windows-tools/`
📖 **Docs**: [windows-tools/README.md](windows-tools/README.md)
**Key Differences:**
- Windows: UEFI + TPM 2.0 + Secure Boot
- Linux: BIOS/UEFI flexible
- Windows: Requires qcow2 image from [cloudbase/windows-imaging-tools](https://github.com/cloudbase/windows-imaging-tools)
- Linux: Download langsung dari official repos
## Requirements ## Requirements
- Go 1.19+ - Go 1.19+
@@ -242,7 +262,10 @@ proxmox-cloud-image -batch batch.txt
## How It Works ## How It Works
1. Download cloud image dari URL yang diberikan 1. Download cloud image dari URL yang diberikan
2. Customize image (resize, inject SSH key jika ada) 2. Customize image:
- Resize disk (jika di-specify)
- Inject SSH key (jika ada)
- **Install qemu-guest-agent package** (jika guest-agent enabled)
3. Upload image ke Proxmox host via SCP 3. Upload image ke Proxmox host via SCP
4. Create VM menggunakan `qm` commands 4. Create VM menggunakan `qm` commands
5. Import disk dan configure VM 5. Import disk dan configure VM
@@ -260,11 +283,15 @@ QEMU Guest Agent adalah service yang berjalan di guest OS untuk:
- File system freeze/thaw - File system freeze/thaw
- Time synchronization - Time synchronization
**Tool ini akan otomatis install qemu-guest-agent package** ke dalam image menggunakan `virt-customize` sebelum upload ke Proxmox.
Enable dengan flag `-guest-agent` atau di config file: Enable dengan flag `-guest-agent` atau di config file:
```yaml ```yaml
guest_agent: true guest_agent: true
``` ```
**Note**: Guest agent di-enable by default. Package akan di-install otomatis saat customize image.
## Proxmox Firewall ## Proxmox Firewall
Proxmox firewall bisa di-enable untuk template dengan flag `-firewall` atau di config file: Proxmox firewall bisa di-enable untuk template dengan flag `-firewall` atau di config file:

View File

@@ -4,7 +4,7 @@ vm_id: 100020
storage: "local" storage: "local"
memory: 1024 memory: 1024
cores: 1 cores: 1
disk_size: "10G" disk_size: "40G"
bridge: "vmbr1" bridge: "vmbr1"
vlan_tag: 301 vlan_tag: 301
proxmox_host: "10.10.26.11" proxmox_host: "10.10.26.11"

View File

@@ -4,7 +4,7 @@ vm_id: 100021
storage: "local" storage: "local"
memory: 1024 memory: 1024
cores: 1 cores: 1
disk_size: "10G" disk_size: "40G"
bridge: "vmbr1" bridge: "vmbr1"
vlan_tag: 301 vlan_tag: 301
proxmox_host: "10.10.26.11" proxmox_host: "10.10.26.11"

View File

@@ -4,7 +4,7 @@ vm_id: 100022
storage: "local" storage: "local"
memory: 1024 memory: 1024
cores: 1 cores: 1
disk_size: "10G" disk_size: "40G"
bridge: "vmbr1" bridge: "vmbr1"
vlan_tag: 301 vlan_tag: 301
proxmox_host: "10.10.26.11" proxmox_host: "10.10.26.11"

View File

@@ -1,4 +1,4 @@
image_url: "https://mirrors.bfsu.edu.cn/fedora/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2" image_url: "https://dl.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-41-1.4.x86_64.qcow2"
vm_name: "cloudimg-fedora-41" vm_name: "cloudimg-fedora-41"
vm_id: 100008 vm_id: 100008
storage: "local" storage: "local"

View File

@@ -1,4 +1,4 @@
image_url: "https://mirrors.bfsu.edu.cn/fedora/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2" image_url: "https://dl.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2"
vm_name: "cloudimg-fedora-42" vm_name: "cloudimg-fedora-42"
vm_id: 100009 vm_id: 100009
storage: "local" storage: "local"

View File

@@ -1,4 +1,4 @@
image_url: "https://mirrors.bfsu.edu.cn/fedora/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2" image_url: "https://dl.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2"
vm_name: "cloudimg-fedora-43" vm_name: "cloudimg-fedora-43"
vm_id: 100010 vm_id: 100010
storage: "local" storage: "local"

View File

@@ -4,7 +4,7 @@ vm_id: 100016
storage: "local" storage: "local"
memory: 1024 memory: 1024
cores: 1 cores: 1
disk_size: "10G" disk_size: "40G"
bridge: "vmbr1" bridge: "vmbr1"
vlan_tag: 301 vlan_tag: 301
proxmox_host: "10.10.26.11" proxmox_host: "10.10.26.11"

View File

@@ -4,7 +4,7 @@ vm_id: 100014
storage: "local" storage: "local"
memory: 1024 memory: 1024
cores: 1 cores: 1
disk_size: "10G" disk_size: "40G"
bridge: "vmbr1" bridge: "vmbr1"
vlan_tag: 301 vlan_tag: 301
proxmox_host: "10.10.26.11" proxmox_host: "10.10.26.11"
@@ -16,4 +16,4 @@ firewall_rules:
- type: out - type: out
action: drop action: drop
dest: "10.0.0.0/8" dest: "10.0.0.0/8"
comment: "Bottom most rules - Block service network to internal network" comment: "Bottom most rules - Block service network to internal network"

View File

@@ -4,7 +4,7 @@ vm_id: 100015
storage: "local" storage: "local"
memory: 1024 memory: 1024
cores: 1 cores: 1
disk_size: "10G" disk_size: "40G"
bridge: "vmbr1" bridge: "vmbr1"
vlan_tag: 301 vlan_tag: 301
proxmox_host: "10.10.26.11" proxmox_host: "10.10.26.11"

View File

@@ -28,6 +28,12 @@ func customizeImage(config *Config) error {
} }
} }
if config.GuestAgent {
if err := installGuestAgent(imagePath); err != nil {
return err
}
}
fmt.Println("Verifying customized image...") fmt.Println("Verifying customized image...")
if err := verifyImage(imagePath); err != nil { if err := verifyImage(imagePath); err != nil {
return fmt.Errorf("customized image verification failed: %w", err) return fmt.Errorf("customized image verification failed: %w", err)
@@ -93,3 +99,22 @@ func injectSSHKey(imagePath, sshKeyPath string) error {
return nil return nil
} }
func installGuestAgent(imagePath string) error {
fmt.Println("Installing QEMU Guest Agent...")
cmd := exec.Command("virt-customize",
"-a", imagePath,
"--install", "qemu-guest-agent",
"--run-command", "systemctl enable qemu-guest-agent",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to install qemu-guest-agent: %w", err)
}
fmt.Println("QEMU Guest Agent installed and enabled!")
return nil
}

256
windows-tools/README.md Normal file
View File

@@ -0,0 +1,256 @@
# Proxmox Windows Cloud Image Tool
Tool untuk membuat Proxmox VM template dari Windows cloud images (qcow2 format).
## Features
- ✅ Upload Windows cloud image (qcow2) ke Proxmox
- ✅ Create VM dengan konfigurasi Windows (UEFI, TPM 2.0, Secure Boot)
- ✅ Convert VM ke template
- ✅ Support QEMU Guest Agent
- ✅ Support firewall configuration
- ✅ Support VLAN tagging
- ✅ Auto-find available VM ID
- ✅ Batch processing multiple configs
- ✅ List available storage
## Requirements
- Go 1.19+
- SSH access ke Proxmox host
- Windows cloud image dalam format qcow2
## Cara Buat Windows Cloud Image
Gunakan [cloudbase/windows-imaging-tools](https://github.com/cloudbase/windows-imaging-tools):
```powershell
# Clone repo
git clone https://github.com/cloudbase/windows-imaging-tools.git
cd windows-imaging-tools
# Import module
Import-Module .\WinImageBuilder.psm1
# Download Windows ISO (contoh: Windows Server 2022)
# Buat cloud image
$ConfigFilePath = ".\Examples\config.ini"
New-WindowsCloudImage -ConfigFilePath $ConfigFilePath
# Output: windows-server-2022.qcow2
```
## Installation
### Linux:
```bash
cd windows-tools
go build -o proxmox-windows
sudo cp proxmox-windows /usr/local/bin/
```
### Windows (PowerShell):
```powershell
cd windows-tools
go build -o proxmox-windows.exe
# Add to PATH atau jalankan langsung
```
## Usage
### CLI Flags:
```bash
proxmox-windows \
-image-path /path/to/windows-server-2022.qcow2 \
-vm-name windows-server-2022-template \
-vm-id 0 \
-storage local-lvm \
-memory 4096 \
-cores 2 \
-bridge vmbr0 \
-proxmox-host 192.168.1.100 \
-proxmox-user root@pam \
-guest-agent \
-firewall
```
### Config File (Recommended):
```bash
proxmox-windows -config config.yaml
```
**config.yaml:**
```yaml
image_path: "/path/to/windows-server-2022.qcow2"
vm_name: "windows-server-2022-template"
vm_id: 0 # 0 = auto-find
storage: "local-lvm"
memory: 4096
cores: 2
sockets: 1
bridge: "vmbr0"
vlan_tag: 0
proxmox_host: "192.168.1.100"
proxmox_user: "root@pam"
guest_agent: true
firewall: true
firewall_rules:
- type: out
action: drop
dest: "10.0.0.0/8"
comment: "Block internal network"
- type: in
action: accept
protocol: tcp
dport: "3389"
comment: "Allow RDP"
- type: in
action: accept
protocol: tcp
dport: "5985-5986"
comment: "Allow WinRM"
```
### Batch Processing:
```bash
proxmox-windows -batch batch.txt
```
**batch.txt:**
```
configs/windows-server-2022.yaml
configs/windows-11.yaml
configs/windows-server-2019.yaml
```
### List Available Storage:
```bash
proxmox-windows -list-storage -proxmox-host 192.168.1.100
# atau
proxmox-windows -ls -proxmox-host 192.168.1.100
```
## VM Configuration
Tool ini akan membuat VM dengan konfigurasi:
- **BIOS**: OVMF (UEFI)
- **Machine Type**: q35
- **EFI Disk**: 1GB dengan pre-enrolled keys (Secure Boot)
- **TPM**: v2.0 (required untuk Windows 11)
- **SCSI Controller**: VirtIO SCSI
- **Network**: VirtIO
- **Boot Order**: scsi0
- **Guest Agent**: Enabled (optional)
## Firewall Rules
Format firewall rules sama dengan Linux tools:
```yaml
firewall_rules:
- type: in|out
action: accept|drop|reject
protocol: tcp|udp|icmp
dport: "port atau port-range"
sport: "port atau port-range"
source: "IP/CIDR"
dest: "IP/CIDR"
comment: "Description"
```
**Contoh:**
```yaml
firewall_rules:
# Allow RDP
- type: in
action: accept
protocol: tcp
dport: "3389"
comment: "Allow RDP"
# Allow WinRM
- type: in
action: accept
protocol: tcp
dport: "5985-5986"
comment: "Allow WinRM"
# Block outgoing to internal network
- type: out
action: drop
dest: "10.0.0.0/8"
comment: "Block internal network"
# Allow DNS
- type: out
action: accept
protocol: udp
dport: "53"
comment: "Allow DNS"
```
## Clone Template
Setelah template dibuat:
```bash
# Clone template
qm clone 10000 101 --name windows-server-01
# Start VM
qm start 101
# Access via RDP
# IP akan di-assign via DHCP (jika cloud-init configured)
```
## Troubleshooting
### 1. SSH Connection Failed
```bash
# Test SSH connection
ssh root@192.168.1.100
# Check SSH key
ssh-keygen -R 192.168.1.100
```
### 2. Storage Not Found
```bash
# List available storage
proxmox-windows -ls -proxmox-host 192.168.1.100
# Atau manual
ssh root@192.168.1.100 'pvesm status'
```
### 3. Upload Failed
- Check network connection
- Check disk space di Proxmox host
- Tool akan retry 3x otomatis
### 4. VM Creation Failed
```bash
# Check Proxmox logs
ssh root@192.168.1.100 'tail -f /var/log/pve/tasks/active'
```
## Notes
- Windows cloud images harus dalam format **qcow2**
- VM akan dibuat dengan **UEFI + TPM 2.0** (required untuk Windows 11)
- Guest Agent harus di-install di Windows image untuk full functionality
- Firewall rules di-apply di Proxmox level, bukan di Windows
- Template bisa di-clone unlimited times
## License
MIT

3
windows-tools/batch.txt Normal file
View File

@@ -0,0 +1,3 @@
configs/windows-server-2022.yaml
configs/windows-11.yaml
configs/windows-server-2019.yaml

130
windows-tools/config.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"bufio"
"fmt"
"os"
"strings"
"sync"
"gopkg.in/yaml.v3"
)
func loadConfig(filename string, config *WindowsConfig) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
if err := yaml.Unmarshal(data, config); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
return nil
}
func processBatchFile(batchFile string) error {
file, err := os.Open(batchFile)
if err != nil {
return fmt.Errorf("failed to open batch file: %w", err)
}
defer file.Close()
var configFiles []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
configFiles = append(configFiles, line)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("failed to read batch file: %w", err)
}
if len(configFiles) == 0 {
return fmt.Errorf("no config files found in batch file")
}
fmt.Printf("Processing %d config files...\n\n", len(configFiles))
maxConcurrent := 3
semaphore := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
var mu sync.Mutex
results := make(map[string]error)
for _, configFile := range configFiles {
wg.Add(1)
go func(cf string) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
fmt.Printf("Processing: %s\n", cf)
config := &WindowsConfig{}
if err := loadConfig(cf, config); err != nil {
mu.Lock()
results[cf] = err
mu.Unlock()
fmt.Printf("Error loading %s: %v\n", cf, err)
return
}
if config.VMID == 0 {
vmid, err := findAvailableVMID(config)
if err != nil {
mu.Lock()
results[cf] = err
mu.Unlock()
fmt.Printf("Error finding VM ID for %s: %v\n", cf, err)
return
}
config.VMID = vmid
}
if err := createProxmoxWindowsVM(config); err != nil {
mu.Lock()
results[cf] = err
mu.Unlock()
fmt.Printf("Error creating template from %s: %v\n", cf, err)
return
}
mu.Lock()
results[cf] = nil
mu.Unlock()
fmt.Printf("Successfully created template from %s\n", cf)
}(configFile)
}
wg.Wait()
fmt.Println("\n========================================")
fmt.Println("Batch Processing Summary")
fmt.Println("========================================")
successCount := 0
failCount := 0
for configFile, err := range results {
if err == nil {
fmt.Printf("✓ %s - SUCCESS\n", configFile)
successCount++
} else {
fmt.Printf("✗ %s - FAILED: %v\n", configFile, err)
failCount++
}
}
fmt.Printf("\nTotal: %d | Success: %d | Failed: %d\n", len(configFiles), successCount, failCount)
if failCount > 0 {
return fmt.Errorf("%d config(s) failed to process", failCount)
}
return nil
}

31
windows-tools/config.yaml Normal file
View File

@@ -0,0 +1,31 @@
# Windows Cloud Image Configuration
image_path: "/path/to/windows-server-2022.qcow2"
vm_name: "windows-server-2022-template"
vm_id: 0 # 0 = auto-find from 10000+
storage: "local-lvm"
memory: 4096
cores: 2
sockets: 1
bridge: "vmbr0"
vlan_tag: 0 # 0 = no VLAN
proxmox_host: "192.168.1.100"
proxmox_user: "root@pam"
guest_agent: true
firewall: true
# Firewall Rules (optional)
firewall_rules:
- type: out
action: drop
dest: "10.0.0.0/8"
comment: "Block service network to internal network"
- type: in
action: accept
protocol: tcp
dport: "3389"
comment: "Allow RDP"
- type: in
action: accept
protocol: tcp
dport: "5985-5986"
comment: "Allow WinRM"

View File

@@ -0,0 +1,30 @@
image_path: "/path/to/windows-11.qcow2"
vm_name: "windows-11-template"
vm_id: 0
storage: "local-lvm"
memory: 8192
cores: 4
sockets: 1
bridge: "vmbr0"
vlan_tag: 0
proxmox_host: "192.168.1.100"
proxmox_user: "root@pam"
guest_agent: true
firewall: true
firewall_rules:
- type: in
action: accept
protocol: tcp
dport: "3389"
comment: "Allow RDP"
- type: out
action: accept
protocol: tcp
dport: "80,443"
comment: "Allow HTTP/HTTPS"
- type: out
action: accept
protocol: udp
dport: "53"
comment: "Allow DNS"

View File

@@ -0,0 +1,13 @@
image_path: "/path/to/windows-server-2019.qcow2"
vm_name: "windows-server-2019-template"
vm_id: 0
storage: "local-lvm"
memory: 4096
cores: 2
sockets: 1
bridge: "vmbr0"
vlan_tag: 0
proxmox_host: "192.168.1.100"
proxmox_user: "root@pam"
guest_agent: true
firewall: false

View File

@@ -0,0 +1,29 @@
image_path: "/path/to/windows-server-2022.qcow2"
vm_name: "windows-server-2022-template"
vm_id: 0
storage: "local-lvm"
memory: 4096
cores: 2
sockets: 1
bridge: "vmbr0"
vlan_tag: 0
proxmox_host: "192.168.1.100"
proxmox_user: "root@pam"
guest_agent: true
firewall: true
firewall_rules:
- type: in
action: accept
protocol: tcp
dport: "3389"
comment: "Allow RDP"
- type: in
action: accept
protocol: tcp
dport: "5985-5986"
comment: "Allow WinRM"
- type: out
action: drop
dest: "10.0.0.0/8"
comment: "Block internal network"

5
windows-tools/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/yourusername/proxmox-windows-tools
go 1.21
require gopkg.in/yaml.v3 v3.0.1

2
windows-tools/go.sum Normal file
View File

@@ -0,0 +1,2 @@
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

42
windows-tools/install.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
set -e
INSTALL_DIR="/usr/local/bin"
BINARY_NAME="proxmox-windows"
echo "=========================================="
echo "Proxmox Windows Tool Installer"
echo "=========================================="
echo ""
if [ "$EUID" -ne 0 ]; then
echo "Error: This script must be run as root (use sudo)"
exit 1
fi
if ! command -v go &> /dev/null; then
echo "Error: Go is not installed. Please install Go 1.19+ first."
echo "Visit: https://golang.org/doc/install"
exit 1
fi
echo "Building binary..."
go build -o $BINARY_NAME
echo "Installing to $INSTALL_DIR..."
cp $BINARY_NAME $INSTALL_DIR/
chmod +x $INSTALL_DIR/$BINARY_NAME
echo ""
echo "=========================================="
echo "Installation completed successfully!"
echo "=========================================="
echo ""
echo "You can now use: $BINARY_NAME"
echo ""
echo "Examples:"
echo " $BINARY_NAME -h"
echo " $BINARY_NAME -config config.yaml"
echo " $BINARY_NAME -list-storage -proxmox-host 192.168.1.100"
echo ""

127
windows-tools/main.go Normal file
View File

@@ -0,0 +1,127 @@
package main
import (
"flag"
"fmt"
"os"
)
type FirewallRule struct {
Type string `yaml:"type"`
Action string `yaml:"action"`
Protocol string `yaml:"protocol"`
Dport string `yaml:"dport"`
Sport string `yaml:"sport"`
Source string `yaml:"source"`
Dest string `yaml:"dest"`
Comment string `yaml:"comment"`
}
type WindowsConfig struct {
ImagePath string `yaml:"image_path"`
VMName string `yaml:"vm_name"`
VMID int `yaml:"vm_id"`
Storage string `yaml:"storage"`
Memory int `yaml:"memory"`
Cores int `yaml:"cores"`
Sockets int `yaml:"sockets"`
Bridge string `yaml:"bridge"`
VlanTag int `yaml:"vlan_tag"`
ProxmoxHost string `yaml:"proxmox_host"`
ProxmoxUser string `yaml:"proxmox_user"`
ProxmoxPass string `yaml:"proxmox_pass"`
GuestAgent bool `yaml:"guest_agent"`
Firewall bool `yaml:"firewall"`
FirewallRules []FirewallRule `yaml:"firewall_rules"`
}
func main() {
config := &WindowsConfig{}
var configFile string
var batchFile string
var listStorage bool
flag.StringVar(&configFile, "config", "", "Config file path (YAML)")
flag.StringVar(&batchFile, "batch", "", "Batch file with multiple config paths (one per line)")
flag.BoolVar(&listStorage, "list-storage", false, "List available storage on Proxmox host")
flag.BoolVar(&listStorage, "ls", false, "List available storage on Proxmox host (shorthand)")
flag.StringVar(&config.ImagePath, "image-path", "", "Windows cloud image path (local qcow2 file)")
flag.StringVar(&config.VMName, "vm-name", "windows-vm", "VM name")
flag.IntVar(&config.VMID, "vm-id", 0, "VM ID (0 = auto-find from 10000+)")
flag.StringVar(&config.Storage, "storage", "", "Proxmox storage name")
flag.IntVar(&config.Memory, "memory", 4096, "Memory in MB")
flag.IntVar(&config.Cores, "cores", 2, "CPU cores")
flag.IntVar(&config.Sockets, "sockets", 1, "CPU sockets")
flag.StringVar(&config.Bridge, "bridge", "vmbr0", "Network bridge")
flag.IntVar(&config.VlanTag, "vlan-tag", 0, "VLAN tag (optional, 0 = no VLAN)")
flag.StringVar(&config.ProxmoxHost, "proxmox-host", "", "Proxmox host (e.g., 192.168.1.100)")
flag.StringVar(&config.ProxmoxUser, "proxmox-user", "root@pam", "Proxmox user")
flag.StringVar(&config.ProxmoxPass, "proxmox-pass", "", "Proxmox password")
flag.BoolVar(&config.GuestAgent, "guest-agent", true, "Enable QEMU guest agent")
flag.BoolVar(&config.Firewall, "firewall", false, "Enable firewall")
flag.Parse()
if listStorage || flag.Lookup("ls").Value.(flag.Getter).Get().(bool) {
if config.ProxmoxHost == "" {
fmt.Println("Error: -proxmox-host is required for storage listing")
os.Exit(1)
}
if err := listAvailableStorage(config); err != nil {
fmt.Printf("Error listing storage: %v\n", err)
os.Exit(1)
}
return
}
if batchFile != "" {
if err := processBatchFile(batchFile); err != nil {
fmt.Printf("Error processing batch file: %v\n", err)
os.Exit(1)
}
return
}
if configFile != "" {
if err := loadConfig(configFile, config); err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
}
if config.ImagePath == "" {
fmt.Println("Error: -image-path or config file is required")
flag.Usage()
os.Exit(1)
}
if config.ProxmoxHost == "" {
fmt.Println("Error: -proxmox-host is required")
flag.Usage()
os.Exit(1)
}
if config.Storage == "" {
fmt.Println("Error: -storage is required")
flag.Usage()
os.Exit(1)
}
if config.VMID == 0 {
fmt.Println("Auto-finding available VM ID starting from 10000...")
vmid, err := findAvailableVMID(config)
if err != nil {
fmt.Printf("Error finding available VM ID: %v\n", err)
os.Exit(1)
}
config.VMID = vmid
fmt.Printf("Found available VM ID: %d\n", config.VMID)
}
fmt.Printf("Creating template %s (ID: %d) on Proxmox host %s\n", config.VMName, config.VMID, config.ProxmoxHost)
if err := createProxmoxWindowsVM(config); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}

View 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
}

27
windows-tools/uninstall.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -e
INSTALL_DIR="/usr/local/bin"
BINARY_NAME="proxmox-windows"
echo "=========================================="
echo "Proxmox Windows Tool Uninstaller"
echo "=========================================="
echo ""
if [ "$EUID" -ne 0 ]; then
echo "Error: This script must be run as root (use sudo)"
exit 1
fi
if [ -f "$INSTALL_DIR/$BINARY_NAME" ]; then
echo "Removing $INSTALL_DIR/$BINARY_NAME..."
rm -f "$INSTALL_DIR/$BINARY_NAME"
echo "Uninstallation completed successfully!"
else
echo "Binary not found at $INSTALL_DIR/$BINARY_NAME"
echo "Nothing to uninstall."
fi
echo ""