Initial commit: Proxmox cloud image template tool
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
proxmox-cloud-image
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
go.work
|
||||||
172
README.md
Normal file
172
README.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# Proxmox Cloud Image Tool
|
||||||
|
|
||||||
|
Tool untuk membuat **template** di Proxmox menggunakan cloud image (Ubuntu, Debian, CentOS, dll) dengan Golang.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Download cloud image dari URL
|
||||||
|
- Customize image (resize disk, inject SSH key)
|
||||||
|
- Otomatis create template di Proxmox
|
||||||
|
- Support konfigurasi via CLI flags atau YAML file
|
||||||
|
- Progress bar untuk download
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Go 1.19+
|
||||||
|
- SSH access ke Proxmox host
|
||||||
|
- `qemu-img` dan `virt-customize` (libguestfs-tools)
|
||||||
|
|
||||||
|
Install dependencies di Ubuntu/Debian:
|
||||||
|
```bash
|
||||||
|
sudo apt install qemu-utils libguestfs-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Build from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repo-url>
|
||||||
|
cd cloud-image
|
||||||
|
go build -o proxmox-cloud-image
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install globally (Linux):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp proxmox-cloud-image /usr/local/bin/
|
||||||
|
sudo chmod +x /usr/local/bin/proxmox-cloud-image
|
||||||
|
```
|
||||||
|
|
||||||
|
Setelah install, bisa langsung dipanggil dari mana aja:
|
||||||
|
```bash
|
||||||
|
proxmox-cloud-image -h
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Menggunakan CLI flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
proxmox-cloud-image \
|
||||||
|
-image-url "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" \
|
||||||
|
-vm-name "ubuntu-template" \
|
||||||
|
-vm-id 9000 \
|
||||||
|
-proxmox-host "192.168.1.100" \
|
||||||
|
-proxmox-user "root@pam" \
|
||||||
|
-storage "local-lvm" \
|
||||||
|
-memory 2048 \
|
||||||
|
-cores 2 \
|
||||||
|
-disk-size "20G" \
|
||||||
|
-bridge "vmbr0" \
|
||||||
|
-ssh-key "/root/.ssh/id_rsa.pub"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-find VM ID (mulai dari 10000):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
proxmox-cloud-image \
|
||||||
|
-image-url "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" \
|
||||||
|
-vm-name "ubuntu-template" \
|
||||||
|
-proxmox-host "192.168.1.100"
|
||||||
|
```
|
||||||
|
|
||||||
|
Kalo `-vm-id` tidak diisi atau diset `0`, tool akan otomatis cari VM ID kosong mulai dari 10000.
|
||||||
|
|
||||||
|
### Dengan VLAN:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
proxmox-cloud-image \
|
||||||
|
-image-url "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" \
|
||||||
|
-vm-name "ubuntu-template" \
|
||||||
|
-vm-id 9000 \
|
||||||
|
-proxmox-host "192.168.1.100" \
|
||||||
|
-bridge "vmbr0" \
|
||||||
|
-vlan-tag 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Menggunakan config file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
proxmox-cloud-image -config config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Contoh `config.yaml`:
|
||||||
|
```yaml
|
||||||
|
image_url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
|
||||||
|
vm_name: "ubuntu-template"
|
||||||
|
vm_id: 0
|
||||||
|
storage: "local-lvm"
|
||||||
|
memory: 2048
|
||||||
|
cores: 2
|
||||||
|
disk_size: "20G"
|
||||||
|
bridge: "vmbr0"
|
||||||
|
vlan_tag: 100
|
||||||
|
ssh_key: "/root/.ssh/id_rsa.pub"
|
||||||
|
proxmox_host: "192.168.1.100"
|
||||||
|
proxmox_user: "root@pam"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloud Image URLs
|
||||||
|
|
||||||
|
### Ubuntu
|
||||||
|
- Ubuntu 22.04 (Jammy): `https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img`
|
||||||
|
- Ubuntu 20.04 (Focal): `https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img`
|
||||||
|
|
||||||
|
### Debian
|
||||||
|
- Debian 12 (Bookworm): `https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2`
|
||||||
|
- Debian 11 (Bullseye): `https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-generic-amd64.qcow2`
|
||||||
|
|
||||||
|
### CentOS Stream
|
||||||
|
- CentOS Stream 9: `https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2`
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `-config` | - | Path ke config file (YAML) |
|
||||||
|
| `-image-url` | - | URL cloud image (required) |
|
||||||
|
| `-vm-name` | cloud-vm | Nama template |
|
||||||
|
| `-vm-id` | 0 | Template ID (0 = auto-find dari 10000+) |
|
||||||
|
| `-storage` | local-lvm | Nama storage Proxmox |
|
||||||
|
| `-memory` | 2048 | Memory dalam MB |
|
||||||
|
| `-cores` | 2 | Jumlah CPU cores |
|
||||||
|
| `-disk-size` | 20G | Ukuran disk |
|
||||||
|
| `-bridge` | vmbr0 | Network bridge |
|
||||||
|
| `-vlan-tag` | 0 | VLAN tag (0 = no VLAN) |
|
||||||
|
| `-ssh-key` | - | Path ke SSH public key |
|
||||||
|
| `-proxmox-host` | - | IP/hostname Proxmox (required) |
|
||||||
|
| `-proxmox-user` | root@pam | Proxmox user |
|
||||||
|
| `-proxmox-pass` | - | Proxmox password |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. Download cloud image dari URL yang diberikan
|
||||||
|
2. Customize image (resize, inject SSH key jika ada)
|
||||||
|
3. Upload image ke Proxmox host via SCP
|
||||||
|
4. Create VM menggunakan `qm` commands
|
||||||
|
5. Import disk dan configure VM
|
||||||
|
6. Setup cloud-init
|
||||||
|
7. **Convert VM menjadi template** dengan `qm template`
|
||||||
|
|
||||||
|
## Clone Template
|
||||||
|
|
||||||
|
Setelah template dibuat, kamu bisa clone untuk membuat VM baru:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qm clone 9000 100 --name my-vm --full
|
||||||
|
qm set 100 --ipconfig0 ip=192.168.1.100/24,gw=192.168.1.1
|
||||||
|
qm set 100 --sshkeys /root/.ssh/id_rsa.pub
|
||||||
|
qm start 100
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tool ini menggunakan SSH untuk koneksi ke Proxmox
|
||||||
|
- Pastikan SSH key sudah di-setup untuk passwordless login
|
||||||
|
- Image akan di-download ke `/tmp` dan di-upload ke Proxmox
|
||||||
|
- Template tidak bisa di-start, harus di-clone dulu
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
13
config.example.yaml
Normal file
13
config.example.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
image_url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
|
||||||
|
vm_name: "ubuntu-template"
|
||||||
|
vm_id: 0
|
||||||
|
storage: "local-lvm"
|
||||||
|
memory: 2048
|
||||||
|
cores: 2
|
||||||
|
disk_size: "20G"
|
||||||
|
bridge: "vmbr0"
|
||||||
|
vlan_tag: 0
|
||||||
|
ssh_key: "/root/.ssh/id_rsa.pub"
|
||||||
|
proxmox_host: "192.168.1.100"
|
||||||
|
proxmox_user: "root@pam"
|
||||||
|
proxmox_pass: ""
|
||||||
21
config.go
Normal file
21
config.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadConfigFile(path string, config *Config) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
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
|
||||||
|
}
|
||||||
89
customize.go
Normal file
89
customize.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func customizeImage(config *Config) error {
|
||||||
|
fmt.Println("Customizing image...")
|
||||||
|
|
||||||
|
if err := checkDependencies(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePath := config.ImageURL
|
||||||
|
if strings.HasSuffix(imagePath, ".qcow2") {
|
||||||
|
if err := resizeImage(imagePath, config.DiskSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SSHKey != "" {
|
||||||
|
if err := injectSSHKey(imagePath, config.SSHKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDependencies() error {
|
||||||
|
deps := []string{"qemu-img", "virt-customize"}
|
||||||
|
for _, dep := range deps {
|
||||||
|
if _, err := exec.LookPath(dep); err != nil {
|
||||||
|
return fmt.Errorf("required dependency '%s' not found. Please install libguestfs-tools and qemu-utils", dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizeImage(imagePath, size string) error {
|
||||||
|
fmt.Printf("Resizing image to %s...\n", size)
|
||||||
|
|
||||||
|
cmd := exec.Command("qemu-img", "resize", imagePath, size)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to resize image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func injectSSHKey(imagePath, sshKeyPath string) error {
|
||||||
|
fmt.Printf("Injecting SSH key from %s...\n", sshKeyPath)
|
||||||
|
|
||||||
|
keyData, err := os.ReadFile(sshKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := "/tmp/cloud-init-" + filepath.Base(imagePath)
|
||||||
|
os.MkdirAll(tmpDir, 0755)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
keyFile := filepath.Join(tmpDir, "authorized_keys")
|
||||||
|
if err := os.WriteFile(keyFile, keyData, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write SSH key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("virt-customize",
|
||||||
|
"-a", imagePath,
|
||||||
|
"--mkdir", "/root/.ssh",
|
||||||
|
"--upload", keyFile+":/root/.ssh/authorized_keys",
|
||||||
|
"--chmod", "0600:/root/.ssh/authorized_keys",
|
||||||
|
)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to inject SSH key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
76
download.go
Normal file
76
download.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func downloadImage(config *Config) error {
|
||||||
|
filename := getFilenameFromURL(config.ImageURL)
|
||||||
|
filepath := filepath.Join("/tmp", filename)
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath); err == nil {
|
||||||
|
fmt.Printf("Image already exists at %s, skipping download\n", filepath)
|
||||||
|
config.ImageURL = filepath
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Downloading image from %s...\n", config.ImageURL)
|
||||||
|
|
||||||
|
resp, err := http.Get(config.ImageURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download image: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("bad status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create file: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
counter := &WriteCounter{Total: resp.ContentLength}
|
||||||
|
_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nDownload completed!")
|
||||||
|
config.ImageURL = filepath
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFilenameFromURL(url string) string {
|
||||||
|
parts := strings.Split(url, "/")
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
type WriteCounter struct {
|
||||||
|
Total int64
|
||||||
|
Downloaded int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *WriteCounter) Write(p []byte) (int, error) {
|
||||||
|
n := len(p)
|
||||||
|
wc.Downloaded += int64(n)
|
||||||
|
wc.printProgress()
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wc *WriteCounter) printProgress() {
|
||||||
|
fmt.Printf("\r")
|
||||||
|
if wc.Total > 0 {
|
||||||
|
percent := float64(wc.Downloaded) / float64(wc.Total) * 100
|
||||||
|
fmt.Printf("Downloading... %.0f%% (%d/%d MB)", percent, wc.Downloaded/1024/1024, wc.Total/1024/1024)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Downloading... %d MB", wc.Downloaded/1024/1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/othman/proxmox-cloud-image
|
||||||
|
|
||||||
|
go 1.22.2
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
3
go.sum
Normal file
3
go.sum
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
99
main.go
Normal file
99
main.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ImageURL string `yaml:"image_url"`
|
||||||
|
VMName string `yaml:"vm_name"`
|
||||||
|
VMID int `yaml:"vm_id"`
|
||||||
|
Storage string `yaml:"storage"`
|
||||||
|
Memory int `yaml:"memory"`
|
||||||
|
Cores int `yaml:"cores"`
|
||||||
|
DiskSize string `yaml:"disk_size"`
|
||||||
|
Bridge string `yaml:"bridge"`
|
||||||
|
VlanTag int `yaml:"vlan_tag"`
|
||||||
|
SSHKey string `yaml:"ssh_key"`
|
||||||
|
ProxmoxHost string `yaml:"proxmox_host"`
|
||||||
|
ProxmoxUser string `yaml:"proxmox_user"`
|
||||||
|
ProxmoxPass string `yaml:"proxmox_pass"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := &Config{}
|
||||||
|
var configFile string
|
||||||
|
|
||||||
|
flag.StringVar(&configFile, "config", "", "Config file path (YAML)")
|
||||||
|
flag.StringVar(&config.ImageURL, "image-url", "", "Cloud image URL to download")
|
||||||
|
flag.StringVar(&config.VMName, "vm-name", "cloud-vm", "VM name")
|
||||||
|
flag.IntVar(&config.VMID, "vm-id", 0, "VM ID (0 = auto-find from 10000+)")
|
||||||
|
flag.StringVar(&config.Storage, "storage", "local-lvm", "Proxmox storage name")
|
||||||
|
flag.IntVar(&config.Memory, "memory", 2048, "Memory in MB")
|
||||||
|
flag.IntVar(&config.Cores, "cores", 2, "CPU cores")
|
||||||
|
flag.StringVar(&config.DiskSize, "disk-size", "20G", "Disk size (e.g., 20G)")
|
||||||
|
flag.StringVar(&config.Bridge, "bridge", "vmbr0", "Network bridge")
|
||||||
|
flag.IntVar(&config.VlanTag, "vlan-tag", 0, "VLAN tag (optional, 0 = no VLAN)")
|
||||||
|
flag.StringVar(&config.SSHKey, "ssh-key", "", "SSH public key file path")
|
||||||
|
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.Parse()
|
||||||
|
|
||||||
|
if configFile != "" {
|
||||||
|
if err := loadConfigFile(configFile, config); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error loading config file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ImageURL == "" {
|
||||||
|
fmt.Println("Error: -image-url is required")
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ProxmoxHost == "" {
|
||||||
|
fmt.Println("Error: -proxmox-host is required")
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := run(config); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Template created successfully!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(config *Config) error {
|
||||||
|
if config.VMID == 0 {
|
||||||
|
fmt.Println("Auto-finding available VM ID starting from 10000...")
|
||||||
|
vmid, err := findAvailableVMID(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find available VM ID: %w", err)
|
||||||
|
}
|
||||||
|
config.VMID = vmid
|
||||||
|
fmt.Printf("Found available VM ID: %d\n", vmid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Creating template %s (ID: %d) on Proxmox host %s\n", config.VMName, config.VMID, config.ProxmoxHost)
|
||||||
|
|
||||||
|
if err := downloadImage(config); err != nil {
|
||||||
|
return fmt.Errorf("failed to download image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := customizeImage(config); err != nil {
|
||||||
|
return fmt.Errorf("failed to customize image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createProxmoxVM(config); err != nil {
|
||||||
|
return fmt.Errorf("failed to create Proxmox template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
143
proxmox.go
Normal file
143
proxmox.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func findAvailableVMID(config *Config) (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 *Config) string {
|
||||||
|
netConfig := fmt.Sprintf("virtio,bridge=%s", config.Bridge)
|
||||||
|
if config.VlanTag > 0 {
|
||||||
|
netConfig += fmt.Sprintf(",tag=%d", config.VlanTag)
|
||||||
|
}
|
||||||
|
return netConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func createProxmoxVM(config *Config) error {
|
||||||
|
fmt.Println("Creating Proxmox 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)
|
||||||
|
|
||||||
|
fmt.Println("Uploading image to Proxmox host...")
|
||||||
|
cmd := scpCmd(config.ImageURL, remotePath)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to upload image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Creating VM and converting to template...")
|
||||||
|
commands := [][]string{
|
||||||
|
{"qm", "create", fmt.Sprintf("%d", config.VMID),
|
||||||
|
"--name", config.VMName,
|
||||||
|
"--memory", fmt.Sprintf("%d", config.Memory),
|
||||||
|
"--cores", fmt.Sprintf("%d", config.Cores),
|
||||||
|
"--net0", buildNetworkConfig(config),
|
||||||
|
},
|
||||||
|
{"qm", "importdisk", fmt.Sprintf("%d", config.VMID), remotePath, config.Storage},
|
||||||
|
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||||
|
"--scsihw", "virtio-scsi-pci",
|
||||||
|
"--scsi0", fmt.Sprintf("%s:vm-%d-disk-0", config.Storage, config.VMID),
|
||||||
|
},
|
||||||
|
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||||
|
"--ide2", fmt.Sprintf("%s:cloudinit", config.Storage),
|
||||||
|
},
|
||||||
|
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||||
|
"--boot", "c",
|
||||||
|
"--bootdisk", "scsi0",
|
||||||
|
},
|
||||||
|
{"qm", "set", fmt.Sprintf("%d", config.VMID),
|
||||||
|
"--serial0", "socket",
|
||||||
|
"--vga", "serial0",
|
||||||
|
},
|
||||||
|
{"qm", "template", fmt.Sprintf("%d", config.VMID)},
|
||||||
|
{"rm", "-f", remotePath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmdArgs := range commands {
|
||||||
|
fmt.Printf("Running: %s\n", strings.Join(cmdArgs, " "))
|
||||||
|
cmd := sshCmd(cmdArgs...)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Println(stdout.String())
|
||||||
|
fmt.Println(stderr.String())
|
||||||
|
return fmt.Errorf("failed to execute command '%s': %w", strings.Join(cmdArgs, " "), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stdout.Len() > 0 {
|
||||||
|
fmt.Println(stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nTemplate %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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user