diff --git a/customize.go b/customize.go index 8675a8d..9b5a652 100644 --- a/customize.go +++ b/customize.go @@ -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 } @@ -43,15 +49,15 @@ func checkDependencies() error { 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 } diff --git a/download.go b/download.go index 8f78018..218d835 100644 --- a/download.go +++ b/download.go @@ -1,12 +1,15 @@ package main import ( + "crypto/sha256" "fmt" "io" "net/http" "os" + "os/exec" "path/filepath" "strings" + "time" ) func downloadImage(config *Config) error { @@ -14,37 +17,108 @@ func downloadImage(config *Config) error { filepath := filepath.Join("/tmp", filename) 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 + 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) + + resp, err := http.Get(config.ImageURL) + if err != nil { + 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() + + if resp.StatusCode != http.StatusOK { + if attempt == maxRetries { + return fmt.Errorf("bad status: %s", resp.Status) + } + fmt.Printf("Bad status: %s\n", resp.Status) + continue + } + + 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 { + 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("Image verification passed!") config.ImageURL = filepath return nil } - fmt.Printf("Downloading image from %s...\n", config.ImageURL) + return fmt.Errorf("failed to download and verify image after %d attempts", maxRetries) +} - resp, err := http.Get(config.ImageURL) +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("failed to download image: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status: %s", resp.Status) + return fmt.Errorf("qemu-img check failed: %w\nOutput: %s", err, string(output)) } - out, err := os.Create(filepath) + fmt.Printf("Computing SHA256 checksum...\n") + file, err := os.Open(filepath) if err != nil { - return fmt.Errorf("failed to create file: %w", err) + return fmt.Errorf("failed to open file for checksum: %w", err) } - defer out.Close() + defer file.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) + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return fmt.Errorf("failed to compute checksum: %w", err) } - fmt.Println("\nDownload completed!") - config.ImageURL = filepath + checksum := fmt.Sprintf("%x", hash.Sum(nil)) + fmt.Printf("SHA256: %s\n", checksum) + return nil } diff --git a/proxmox.go b/proxmox.go index e3344f2..566170f 100644 --- a/proxmox.go +++ b/proxmox.go @@ -82,11 +82,44 @@ func createProxmoxVM(config *Config) error { 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) + 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.Stdout = os.Stdout + cmd.Stderr = os.Stderr + uploadErr = cmd.Run() + + 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...") @@ -119,20 +152,35 @@ func createProxmoxVM(config *Config) error { 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 + 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 + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + cmdErr = cmd.Run() + + if cmdErr == nil { + if stdout.Len() > 0 { + fmt.Println(stdout.String()) + } + break + } - 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()) + 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) } }