Add support for local image files (URL or local path)

This commit is contained in:
2025-11-18 16:00:29 +07:00
parent 5690f50cfc
commit 651ca6ecd3
3 changed files with 162 additions and 7 deletions

View File

@@ -4,7 +4,7 @@ Tool untuk membuat **template** di Proxmox menggunakan cloud image (Ubuntu, Debi
## Features ## Features
- Download cloud image dari URL - Download cloud image dari URL **atau gunakan local file**
- Customize image (resize disk, inject SSH key) - Customize image (resize disk, inject SSH key)
- Otomatis create template di Proxmox - Otomatis create template di Proxmox
- Support konfigurasi via CLI flags atau YAML file - Support konfigurasi via CLI flags atau YAML file
@@ -120,7 +120,7 @@ proxmox-cloud-image -h
## Usage ## Usage
### Menggunakan CLI flags: ### Menggunakan URL (download):
```bash ```bash
proxmox-cloud-image \ proxmox-cloud-image \
@@ -139,6 +139,18 @@ proxmox-cloud-image \
-firewall -firewall
``` ```
### Menggunakan local file:
```bash
proxmox-cloud-image \
-image-url "/path/to/ubuntu-22.04-server-cloudimg-amd64.img" \
-vm-name "ubuntu-template" \
-vm-id 9000 \
-proxmox-host "192.168.1.100" \
-storage "local-lvm" \
-guest-agent
```
### Auto-find VM ID (mulai dari 10000): ### Auto-find VM ID (mulai dari 10000):
```bash ```bash
@@ -169,7 +181,7 @@ proxmox-cloud-image \
proxmox-cloud-image -config config.yaml proxmox-cloud-image -config config.yaml
``` ```
Contoh `config.yaml`: Contoh `config.yaml` dengan URL:
```yaml ```yaml
image_url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" image_url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
vm_name: "ubuntu-template" vm_name: "ubuntu-template"
@@ -199,6 +211,23 @@ firewall_rules:
comment: "HTTP/HTTPS" comment: "HTTP/HTTPS"
``` ```
Contoh `config.yaml` dengan local file:
```yaml
image_url: "/home/user/images/ubuntu-22.04-server-cloudimg-amd64.img"
vm_name: "ubuntu-template"
vm_id: 0
storage: "local-lvm"
memory: 2048
cores: 2
disk_size: "20G"
bridge: "vmbr0"
ssh_key: "/root/.ssh/id_rsa.pub"
proxmox_host: "192.168.1.100"
proxmox_user: "root@pam"
guest_agent: true
firewall: false
```
### Batch mode (multiple templates): ### Batch mode (multiple templates):
Buat file batch (contoh: `batch.txt`) dengan list config files: Buat file batch (contoh: `batch.txt`) dengan list config files:
@@ -261,7 +290,7 @@ proxmox-cloud-image -batch batch.txt
## How It Works ## How It Works
1. Download cloud image dari URL yang diberikan 1. **Prepare image** (download dari URL atau copy dari local file)
2. Customize image: 2. Customize image:
- Resize disk (jika di-specify) - Resize disk (jika di-specify)
- Inject SSH key (jika ada) - Inject SSH key (jika ada)

View File

@@ -12,6 +12,132 @@ import (
"time" "time"
) )
func isURL(path string) bool {
return strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://")
}
func downloadOrCopyImage(config *Config) error {
// Check if it's a local file
if !isURL(config.ImageURL) {
return handleLocalImage(config)
}
// It's a URL, download it
return downloadImage(config)
}
func handleLocalImage(config *Config) error {
localPath := config.ImageURL
// Check if file exists
if _, err := os.Stat(localPath); os.IsNotExist(err) {
return fmt.Errorf("local image file not found: %s", localPath)
}
fmt.Printf("Using local image: %s\n", localPath)
// Verify image
if err := verifyImage(localPath); err != nil {
return fmt.Errorf("local image verification failed: %w", err)
}
fmt.Println("Local image verification passed!")
// Check if it's already in /tmp, if not copy it
filename := filepath.Base(localPath)
tmpPath := filepath.Join("/tmp", filename)
absLocal, _ := filepath.Abs(localPath)
absTmp, _ := filepath.Abs(tmpPath)
if absLocal != absTmp {
// Check if already exists in /tmp
if _, err := os.Stat(tmpPath); err == nil {
fmt.Printf("Image already exists in /tmp, verifying...\n")
if err := verifyImage(tmpPath); err != nil {
fmt.Println("Cached image corrupted, copying fresh...")
if err := copyFile(localPath, tmpPath); err != nil {
return fmt.Errorf("failed to copy image to /tmp: %w", err)
}
} else {
fmt.Println("Using cached image from /tmp")
}
} else {
fmt.Printf("Copying image to /tmp...\n")
if err := copyFile(localPath, tmpPath); err != nil {
return fmt.Errorf("failed to copy image to /tmp: %w", err)
}
fmt.Println("Image copied successfully!")
}
config.ImageURL = tmpPath
} else {
// Already in /tmp, ensure config path is normalized
config.ImageURL = tmpPath
}
return nil
}
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
// Get file size for progress
fileInfo, err := sourceFile.Stat()
if err != nil {
return err
}
fileSize := fileInfo.Size()
fmt.Printf("Copying %s (%.2f MB)...\n", filepath.Base(src), float64(fileSize)/(1024*1024))
// Copy with progress
buf := make([]byte, 1024*1024) // 1MB buffer
var written int64
lastProgress := 0
for {
nr, err := sourceFile.Read(buf)
if nr > 0 {
nw, err := destFile.Write(buf[0:nr])
if err != nil {
return err
}
if nr != nw {
return fmt.Errorf("short write")
}
written += int64(nw)
// Show progress every 10%
if fileSize > 0 {
progress := int(float64(written) / float64(fileSize) * 100)
if progress >= lastProgress+10 {
fmt.Printf("Progress: %d%%\n", progress)
lastProgress = progress
}
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
fmt.Println("Progress: 100%")
return nil
}
func downloadImage(config *Config) error { func downloadImage(config *Config) error {
filename := getFilenameFromURL(config.ImageURL) filename := getFilenameFromURL(config.ImageURL)
filepath := filepath.Join("/tmp", filename) filepath := filepath.Join("/tmp", filename)

View File

@@ -46,7 +46,7 @@ func main() {
flag.StringVar(&batchFile, "batch", "", "Batch file with multiple config paths (one per line)") 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, "list-storage", false, "List available storage on Proxmox host")
flag.BoolVar(&listStorage, "ls", false, "List available storage on Proxmox host (shorthand)") flag.BoolVar(&listStorage, "ls", false, "List available storage on Proxmox host (shorthand)")
flag.StringVar(&config.ImageURL, "image-url", "", "Cloud image URL to download") flag.StringVar(&config.ImageURL, "image-url", "", "Cloud image URL or local file path")
flag.StringVar(&config.VMName, "vm-name", "cloud-vm", "VM name") 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.IntVar(&config.VMID, "vm-id", 0, "VM ID (0 = auto-find from 10000+)")
flag.StringVar(&config.Storage, "storage", "", "Proxmox storage name (auto-detect if empty)") flag.StringVar(&config.Storage, "storage", "", "Proxmox storage name (auto-detect if empty)")
@@ -126,8 +126,8 @@ func run(config *Config) error {
fmt.Printf("Creating template %s (ID: %d) on Proxmox host %s\n", config.VMName, config.VMID, config.ProxmoxHost) fmt.Printf("Creating template %s (ID: %d) on Proxmox host %s\n", config.VMName, config.VMID, config.ProxmoxHost)
if err := downloadImage(config); err != nil { if err := downloadOrCopyImage(config); err != nil {
return fmt.Errorf("failed to download image: %w", err) return fmt.Errorf("failed to prepare image: %w", err)
} }
if err := customizeImage(config); err != nil { if err := customizeImage(config); err != nil {