Compare commits
7 Commits
a218640c29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 952a46fd05 | |||
| 651ca6ecd3 | |||
| c3106bb214 | |||
| 5690f50cfc | |||
| 4042ad7263 | |||
| 6aae904e38 | |||
| c22a5dae0a |
46
README.md
46
README.md
@@ -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,8 +290,11 @@ 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 (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
|
||||||
@@ -280,11 +312,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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
25
customize.go
25
customize.go
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
126
download.go
126
download.go
@@ -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)
|
||||||
|
|||||||
6
main.go
6
main.go
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user