Add Windows cloud image tools with firewall support
This commit is contained in:
20
README.md
20
README.md
@@ -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+
|
||||||
|
|||||||
256
windows-tools/README.md
Normal file
256
windows-tools/README.md
Normal 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
3
windows-tools/batch.txt
Normal 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
130
windows-tools/config.go
Normal 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
31
windows-tools/config.yaml
Normal 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"
|
||||||
30
windows-tools/configs/windows-11.yaml
Normal file
30
windows-tools/configs/windows-11.yaml
Normal 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"
|
||||||
13
windows-tools/configs/windows-server-2019.yaml
Normal file
13
windows-tools/configs/windows-server-2019.yaml
Normal 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
|
||||||
29
windows-tools/configs/windows-server-2022.yaml
Normal file
29
windows-tools/configs/windows-server-2022.yaml
Normal 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
5
windows-tools/go.mod
Normal 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
2
windows-tools/go.sum
Normal 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
42
windows-tools/install.sh
Executable 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
127
windows-tools/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
27
windows-tools/uninstall.sh
Executable file
27
windows-tools/uninstall.sh
Executable 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 ""
|
||||||
Reference in New Issue
Block a user