Add checksum verification and retry logic- Add SHA256 checksum verification for downloaded images- Add qemu-img check verification before and after operations- Add retry logic (3 attempts) for download, upload, and commands- Verify image integrity after customization- Verify uploaded image on Proxmox host before import- Auto-remove corrupted images and retry download

This commit is contained in:
2025-11-14 21:14:39 +07:00
parent 8d057bfd94
commit aea1b31115
3 changed files with 163 additions and 35 deletions

View File

@@ -28,6 +28,12 @@ func customizeImage(config *Config) error {
} }
} }
fmt.Println("Verifying customized image...")
if err := verifyImage(imagePath); err != nil {
return fmt.Errorf("customized image verification failed: %w", err)
}
fmt.Println("Customized image verification passed!")
return nil return nil
} }

View File

@@ -1,12 +1,15 @@
package main package main
import ( import (
"crypto/sha256"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
) )
func downloadImage(config *Config) error { func downloadImage(config *Config) error {
@@ -14,22 +17,45 @@ func downloadImage(config *Config) error {
filepath := filepath.Join("/tmp", filename) filepath := filepath.Join("/tmp", filename)
if _, err := os.Stat(filepath); err == nil { if _, err := os.Stat(filepath); err == nil {
fmt.Printf("Image already exists at %s, skipping download\n", filepath) fmt.Printf("Image already exists at %s\n", filepath)
if err := verifyImage(filepath); err != nil {
fmt.Printf("Image verification failed: %v\n", err)
fmt.Println("Removing corrupted image and re-downloading...")
os.Remove(filepath)
} else {
fmt.Println("Image verification passed, skipping download")
config.ImageURL = filepath config.ImageURL = filepath
return nil return nil
} }
}
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
if attempt > 1 {
fmt.Printf("\nRetry attempt %d/%d...\n", attempt, maxRetries)
time.Sleep(time.Second * 2)
}
fmt.Printf("Downloading image from %s...\n", config.ImageURL) fmt.Printf("Downloading image from %s...\n", config.ImageURL)
resp, err := http.Get(config.ImageURL) resp, err := http.Get(config.ImageURL)
if err != nil { if err != nil {
return fmt.Errorf("failed to download image: %w", err) if attempt == maxRetries {
return fmt.Errorf("failed to download image after %d attempts: %w", maxRetries, err)
}
fmt.Printf("Download failed: %v\n", err)
continue
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
if attempt == maxRetries {
return fmt.Errorf("bad status: %s", resp.Status) return fmt.Errorf("bad status: %s", resp.Status)
} }
fmt.Printf("Bad status: %s\n", resp.Status)
continue
}
out, err := os.Create(filepath) out, err := os.Create(filepath)
if err != nil { if err != nil {
@@ -40,14 +66,62 @@ func downloadImage(config *Config) error {
counter := &WriteCounter{Total: resp.ContentLength} counter := &WriteCounter{Total: resp.ContentLength}
_, err = io.Copy(out, io.TeeReader(resp.Body, counter)) _, err = io.Copy(out, io.TeeReader(resp.Body, counter))
if err != nil { if err != nil {
return fmt.Errorf("failed to save image: %w", err) out.Close()
os.Remove(filepath)
if attempt == maxRetries {
return fmt.Errorf("failed to save image after %d attempts: %w", maxRetries, err)
}
fmt.Printf("\nDownload failed: %v\n", err)
continue
}
out.Close()
fmt.Println("\nDownload completed! Verifying image integrity...")
if err := verifyImage(filepath); err != nil {
os.Remove(filepath)
if attempt == maxRetries {
return fmt.Errorf("image verification failed after %d attempts: %w", maxRetries, err)
}
fmt.Printf("Verification failed: %v\n", err)
continue
} }
fmt.Println("\nDownload completed!") fmt.Println("Image verification passed!")
config.ImageURL = filepath config.ImageURL = filepath
return nil return nil
} }
return fmt.Errorf("failed to download and verify image after %d attempts", maxRetries)
}
func verifyImage(filepath string) error {
fmt.Printf("Checking image with qemu-img...\n")
cmd := exec.Command("qemu-img", "check", filepath)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("qemu-img check failed: %w\nOutput: %s", err, string(output))
}
fmt.Printf("Computing SHA256 checksum...\n")
file, err := os.Open(filepath)
if err != nil {
return fmt.Errorf("failed to open file for checksum: %w", err)
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return fmt.Errorf("failed to compute checksum: %w", err)
}
checksum := fmt.Sprintf("%x", hash.Sum(nil))
fmt.Printf("SHA256: %s\n", checksum)
return nil
}
func getFilenameFromURL(url string) string { func getFilenameFromURL(url string) string {
parts := strings.Split(url, "/") parts := strings.Split(url, "/")
return parts[len(parts)-1] return parts[len(parts)-1]

View File

@@ -82,11 +82,44 @@ func createProxmoxVM(config *Config) error {
remotePath := fmt.Sprintf("/tmp/%s", imageName) remotePath := fmt.Sprintf("/tmp/%s", imageName)
fmt.Println("Uploading image to Proxmox host...") fmt.Println("Uploading 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.ImageURL, remotePath) cmd := scpCmd(config.ImageURL, remotePath)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { uploadErr = cmd.Run()
return fmt.Errorf("failed to upload image: %w", err)
if uploadErr == nil {
fmt.Println("Upload completed! Verifying uploaded image...")
verifyCmd := sshCmd("qemu-img", "check", remotePath)
var verifyOut bytes.Buffer
verifyCmd.Stdout = &verifyOut
verifyCmd.Stderr = &verifyOut
if err := verifyCmd.Run(); err != nil {
fmt.Printf("Remote image verification failed: %s\n", verifyOut.String())
if attempt < maxRetries {
fmt.Println("Re-uploading...")
sshCmd("rm", "-f", remotePath).Run()
continue
}
return fmt.Errorf("uploaded image verification failed after %d attempts", maxRetries)
}
fmt.Println("Remote image verification passed!")
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 VM and converting to template...") fmt.Println("Creating VM and converting to template...")
@@ -119,21 +152,36 @@ func createProxmoxVM(config *Config) error {
for _, cmdArgs := range commands { for _, cmdArgs := range commands {
fmt.Printf("Running: %s\n", strings.Join(cmdArgs, " ")) fmt.Printf("Running: %s\n", strings.Join(cmdArgs, " "))
cmd := sshCmd(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 var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout cmd.Stdout = &stdout
cmd.Stderr = &stderr cmd.Stderr = &stderr
if err := cmd.Run(); err != nil { cmdErr = cmd.Run()
fmt.Println(stdout.String())
fmt.Println(stderr.String())
return fmt.Errorf("failed to execute command '%s': %w", strings.Join(cmdArgs, " "), err)
}
if cmdErr == nil {
if stdout.Len() > 0 { if stdout.Len() > 0 {
fmt.Println(stdout.String()) 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)
}
} }
fmt.Printf("\nTemplate %s (ID: %d) created successfully!\n", config.VMName, config.VMID) fmt.Printf("\nTemplate %s (ID: %d) created successfully!\n", config.VMName, config.VMID)