From 651ca6ecd359cc844fc76a89ea09d7a51b1f73dc Mon Sep 17 00:00:00 2001 From: Othman Hendy Suseno Date: Tue, 18 Nov 2025 16:00:29 +0700 Subject: [PATCH] Add support for local image files (URL or local path) --- README.md | 37 +++++++++++++-- download.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 6 +-- 3 files changed, 162 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1221f63..dbda693 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Tool untuk membuat **template** di Proxmox menggunakan cloud image (Ubuntu, Debi ## Features -- Download cloud image dari URL +- Download cloud image dari URL **atau gunakan local file** - Customize image (resize disk, inject SSH key) - Otomatis create template di Proxmox - Support konfigurasi via CLI flags atau YAML file @@ -120,7 +120,7 @@ proxmox-cloud-image -h ## Usage -### Menggunakan CLI flags: +### Menggunakan URL (download): ```bash proxmox-cloud-image \ @@ -139,6 +139,18 @@ proxmox-cloud-image \ -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): ```bash @@ -169,7 +181,7 @@ proxmox-cloud-image \ proxmox-cloud-image -config config.yaml ``` -Contoh `config.yaml`: +Contoh `config.yaml` dengan URL: ```yaml image_url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img" vm_name: "ubuntu-template" @@ -199,6 +211,23 @@ firewall_rules: 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): Buat file batch (contoh: `batch.txt`) dengan list config files: @@ -261,7 +290,7 @@ proxmox-cloud-image -batch batch.txt ## 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: - Resize disk (jika di-specify) - Inject SSH key (jika ada) diff --git a/download.go b/download.go index 218d835..392e5df 100644 --- a/download.go +++ b/download.go @@ -12,6 +12,132 @@ import ( "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 { filename := getFilenameFromURL(config.ImageURL) filepath := filepath.Join("/tmp", filename) diff --git a/main.go b/main.go index 41dd2ab..67f878c 100644 --- a/main.go +++ b/main.go @@ -46,7 +46,7 @@ func main() { 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.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.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)") @@ -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) - if err := downloadImage(config); err != nil { - return fmt.Errorf("failed to download image: %w", err) + if err := downloadOrCopyImage(config); err != nil { + return fmt.Errorf("failed to prepare image: %w", err) } if err := customizeImage(config); err != nil {