package imageformats import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/sirupsen/logrus" ) type MultiFormatGenerator struct { logger *logrus.Logger config *ImageConfig formats map[string]ImageFormat validators map[string]ImageValidator } type ImageConfig struct { OutputDir string `json:"output_dir"` BaseImage string `json:"base_image"` Compression string `json:"compression"` Size string `json:"size"` Format string `json:"format"` Metadata map[string]string `json:"metadata"` } type ImageFormat struct { Name string `json:"name"` Extension string `json:"extension"` Description string `json:"description"` Tools []string `json:"tools"` Options map[string]interface{} `json:"options"` } type ImageValidator struct { Name string `json:"name"` Description string `json:"description"` Commands []string `json:"commands"` } type GeneratedImage struct { ID string `json:"id"` Name string `json:"name"` Format string `json:"format"` Path string `json:"path"` Size int64 `json:"size"` Checksum string `json:"checksum"` Metadata map[string]interface{} `json:"metadata"` CreatedAt time.Time `json:"created_at"` Validated bool `json:"validated"` ValidationResults []ValidationResult `json:"validation_results"` } type ValidationResult struct { Validator string `json:"validator"` Status string `json:"status"` Message string `json:"message"` Details map[string]interface{} `json:"details"` Timestamp time.Time `json:"timestamp"` } func NewMultiFormatGenerator(config *ImageConfig, logger *logrus.Logger) *MultiFormatGenerator { generator := &MultiFormatGenerator{ logger: logger, config: config, formats: make(map[string]ImageFormat), validators: make(map[string]ImageValidator), } // Initialize supported formats generator.initializeFormats() // Initialize validators generator.initializeValidators() return generator } func (mfg *MultiFormatGenerator) initializeFormats() { // ISO format mfg.formats["iso"] = ImageFormat{ Name: "iso", Extension: ".iso", Description: "ISO images for physical media", Tools: []string{"genisoimage", "xorriso", "mkisofs"}, Options: map[string]interface{}{ "bootable": true, "volume_id": "DEBIAN_ATOMIC", "joliet": true, "rock_ridge": true, }, } // QCOW2 format mfg.formats["qcow2"] = ImageFormat{ Name: "qcow2", Extension: ".qcow2", Description: "QCOW2 for virtualization", Tools: []string{"qemu-img", "virt-make-fs"}, Options: map[string]interface{}{ "compression": "zlib", "cluster_size": 65536, "preallocation": "metadata", }, } // RAW format mfg.formats["raw"] = ImageFormat{ Name: "raw", Extension: ".raw", Description: "RAW images for direct disk writing", Tools: []string{"dd", "qemu-img", "virt-make-fs"}, Options: map[string]interface{}{ "sparse": true, "alignment": 4096, }, } // VMDK format mfg.formats["vmdk"] = ImageFormat{ Name: "vmdk", Extension: ".vmdk", Description: "VMDK for VMware compatibility", Tools: []string{"qemu-img", "vmdk-tool"}, Options: map[string]interface{}{ "subformat": "monolithicSparse", "adapter_type": "lsilogic", }, } // TAR format mfg.formats["tar"] = ImageFormat{ Name: "tar", Extension: ".tar.gz", Description: "TAR archives for deployment", Tools: []string{"tar", "gzip"}, Options: map[string]interface{}{ "compression": "gzip", "preserve_permissions": true, }, } } func (mfg *MultiFormatGenerator) initializeValidators() { // ISO validator mfg.validators["iso"] = ImageValidator{ Name: "iso_validator", Description: "Validate ISO image structure and bootability", Commands: []string{"file", "isoinfo", "xorriso"}, } // QCOW2 validator mfg.validators["qcow2"] = ImageValidator{ Name: "qcow2_validator", Description: "Validate QCOW2 image integrity", Commands: []string{"qemu-img", "virt-filesystems"}, } // RAW validator mfg.validators["raw"] = ImageValidator{ Name: "raw_validator", Description: "Validate RAW image structure", Commands: []string{"file", "fdisk", "parted"}, } // VMDK validator mfg.validators["vmdk"] = ImageValidator{ Name: "vmdk_validator", Description: "Validate VMDK image format", Commands: []string{"qemu-img", "vmdk-tool"}, } } func (mfg *MultiFormatGenerator) GenerateImage(blueprint *Blueprint, format string, variant string) (*GeneratedImage, error) { mfg.logger.Infof("Generating %s image for blueprint: %s, variant: %s", format, blueprint.Name, variant) // Check if format is supported imageFormat, exists := mfg.formats[format] if !exists { return nil, fmt.Errorf("unsupported format: %s", format) } // Check if required tools are available if err := mfg.checkTools(imageFormat.Tools); err != nil { return nil, fmt.Errorf("required tools not available: %w", err) } // Create image structure image := &GeneratedImage{ ID: generateImageID(), Name: fmt.Sprintf("%s-%s-%s", blueprint.Name, variant, format), Format: format, Metadata: make(map[string]interface{}), CreatedAt: time.Now(), } // Set output path image.Path = filepath.Join(mfg.config.OutputDir, image.Name+imageFormat.Extension) // Generate image based on format if err := mfg.generateFormatSpecificImage(image, blueprint, variant, imageFormat); err != nil { return nil, fmt.Errorf("failed to generate %s image: %w", format, err) } // Calculate image size if err := mfg.calculateImageSize(image); err != nil { mfg.logger.Warnf("Failed to calculate image size: %v", err) } // Generate checksum if err := mfg.generateChecksum(image); err != nil { mfg.logger.Warnf("Failed to generate checksum: %v", err) } // Validate image if err := mfg.validateImage(image); err != nil { mfg.logger.Warnf("Image validation failed: %v", err) } else { image.Validated = true } // Set metadata image.Metadata["blueprint"] = blueprint.Name image.Metadata["variant"] = variant image.Metadata["format"] = format image.Metadata["tools_used"] = imageFormat.Tools image.Metadata["options"] = imageFormat.Options mfg.logger.Infof("Successfully generated image: %s", image.Path) return image, nil } func (mfg *MultiFormatGenerator) generateFormatSpecificImage(image *GeneratedImage, blueprint *Blueprint, variant string, format ImageFormat) error { switch format.Name { case "iso": return mfg.generateISOImage(image, blueprint, variant, format) case "qcow2": return mfg.generateQCOW2Image(image, blueprint, variant, format) case "raw": return mfg.generateRAWImage(image, blueprint, variant, format) case "vmdk": return mfg.generateVMDKImage(image, blueprint, variant, format) case "tar": return mfg.generateTARImage(image, blueprint, variant, format) default: return fmt.Errorf("unsupported format: %s", format.Name) } } func (mfg *MultiFormatGenerator) generateISOImage(image *GeneratedImage, blueprint *Blueprint, variant string, format ImageFormat) error { // Create temporary directory for ISO contents tempDir, err := os.MkdirTemp("", "debian-iso-*") if err != nil { return fmt.Errorf("failed to create temp directory: %w", err) } defer os.RemoveAll(tempDir) // Create ISO structure if err := mfg.createISOStructure(tempDir, blueprint, variant); err != nil { return fmt.Errorf("failed to create ISO structure: %w", err) } // Generate ISO using genisoimage cmd := exec.Command("genisoimage", "-o", image.Path, "-R", "-J", "-joliet-long", "-b", "isolinux/isolinux.bin", "-no-emul-boot", "-boot-load-size", "4", "-boot-info-table", "-c", "isolinux/boot.cat", "-V", format.Options["volume_id"].(string), tempDir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("genisoimage failed: %w", err) } return nil } func (mfg *MultiFormatGenerator) generateQCOW2Image(image *GeneratedImage, blueprint *Blueprint, variant string, format ImageFormat) error { // Create base raw image first rawImage := &GeneratedImage{ Name: image.Name + "-raw", Format: "raw", Path: filepath.Join(mfg.config.OutputDir, image.Name+"-raw.img"), } if err := mfg.generateRAWImage(rawImage, blueprint, variant, format); err != nil { return fmt.Errorf("failed to generate base raw image: %w", err) } defer os.Remove(rawImage.Path) // Convert to QCOW2 cmd := exec.Command("qemu-img", "convert", "-f", "raw", "-O", "qcow2", "-c", // Enable compression "-o", "compression_type=zlib", rawImage.Path, image.Path) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("qemu-img convert failed: %w", err) } return nil } func (mfg *MultiFormatGenerator) generateRAWImage(image *GeneratedImage, blueprint *Blueprint, variant string, format ImageFormat) error { // Create disk image using dd size := mfg.config.Size if size == "" { size = "10G" } cmd := exec.Command("dd", "if=/dev/zero", "of="+image.Path, "bs=1M", "count=10240") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("dd failed: %w", err) } // Create filesystem if err := mfg.createFilesystem(image.Path, blueprint, variant); err != nil { return fmt.Errorf("failed to create filesystem: %w", err) } return nil } func (mfg *MultiFormatGenerator) generateVMDKImage(image *GeneratedImage, blueprint *Blueprint, variant string, format ImageFormat) error { // Create base raw image first rawImage := &GeneratedImage{ Name: image.Name + "-raw", Format: "raw", Path: filepath.Join(mfg.config.OutputDir, image.Name+"-raw.img"), } if err := mfg.generateRAWImage(rawImage, blueprint, variant, format); err != nil { return fmt.Errorf("failed to generate base raw image: %w", err) } defer os.Remove(rawImage.Path) // Convert to VMDK cmd := exec.Command("qemu-img", "convert", "-f", "raw", "-O", "vmdk", "-o", "subformat=monolithicSparse", rawImage.Path, image.Path) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("qemu-img convert to VMDK failed: %w", err) } return nil } func (mfg *MultiFormatGenerator) generateTARImage(image *GeneratedImage, blueprint *Blueprint, variant string, format ImageFormat) error { // Create temporary directory for TAR contents tempDir, err := os.MkdirTemp("", "debian-tar-*") if err != nil { return fmt.Errorf("failed to create temp directory: %w", err) } defer os.RemoveAll(tempDir) // Create TAR structure if err := mfg.createTARStructure(tempDir, blueprint, variant); err != nil { return fmt.Errorf("failed to create TAR structure: %w", err) } // Create TAR archive cmd := exec.Command("tar", "-czf", image.Path, "-C", tempDir, ".") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("tar creation failed: %w", err) } return nil } func (mfg *MultiFormatGenerator) createISOStructure(tempDir string, blueprint *Blueprint, variant string) error { // Create basic ISO structure dirs := []string{ "isolinux", "boot", "live", "casper", "dists", "pool", } for _, dir := range dirs { if err := os.MkdirAll(filepath.Join(tempDir, dir), 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } } // Create isolinux configuration isolinuxConfig := `DEFAULT live TIMEOUT 300 PROMPT 1 LABEL live menu label ^Live System kernel /casper/vmlinuz append boot=casper initrd=/casper/initrd.lz ` isolinuxPath := filepath.Join(tempDir, "isolinux", "isolinux.cfg") if err := os.WriteFile(isolinuxPath, []byte(isolinuxConfig), 0644); err != nil { return fmt.Errorf("failed to write isolinux config: %w", err) } // Create basic kernel and initrd placeholders kernelPath := filepath.Join(tempDir, "casper", "vmlinuz") initrdPath := filepath.Join(tempDir, "casper", "initrd.lz") if err := os.WriteFile(kernelPath, []byte("placeholder"), 0644); err != nil { return fmt.Errorf("failed to create kernel placeholder: %w", err) } if err := os.WriteFile(initrdPath, []byte("placeholder"), 0644); err != nil { return fmt.Errorf("failed to create initrd placeholder: %w", err) } return nil } func (mfg *MultiFormatGenerator) createFilesystem(imagePath string, blueprint *Blueprint, variant string) error { // Create ext4 filesystem cmd := exec.Command("mkfs.ext4", imagePath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("mkfs.ext4 failed: %w", err) } return nil } func (mfg *MultiFormatGenerator) createTARStructure(tempDir string, blueprint *Blueprint, variant string) error { // Create basic TAR structure dirs := []string{ "etc", "usr", "var", "home", "root", } for _, dir := range dirs { if err := os.MkdirAll(filepath.Join(tempDir, dir), 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } } // Create basic configuration files hostnamePath := filepath.Join(tempDir, "etc", "hostname") hostnameContent := fmt.Sprintf("%s-%s", blueprint.Name, variant) if err := os.WriteFile(hostnamePath, []byte(hostnameContent), 0644); err != nil { return fmt.Errorf("failed to write hostname: %w", err) } return nil } func (mfg *MultiFormatGenerator) checkTools(requiredTools []string) error { var missingTools []string for _, tool := range requiredTools { if !mfg.isToolAvailable(tool) { missingTools = append(missingTools, tool) } } if len(missingTools) > 0 { return fmt.Errorf("missing required tools: %s", strings.Join(missingTools, ", ")) } return nil } func (mfg *MultiFormatGenerator) isToolAvailable(tool string) bool { cmd := exec.Command("which", tool) return cmd.Run() == nil } func (mfg *MultiFormatGenerator) calculateImageSize(image *GeneratedImage) error { info, err := os.Stat(image.Path) if err != nil { return fmt.Errorf("failed to stat image: %w", err) } image.Size = info.Size() return nil } func (mfg *MultiFormatGenerator) generateChecksum(image *GeneratedImage) error { cmd := exec.Command("sha256sum", image.Path) output, err := cmd.Output() if err != nil { return fmt.Errorf("sha256sum failed: %w", err) } // Parse checksum from output (format: "checksum filename") parts := strings.Fields(string(output)) if len(parts) >= 1 { image.Checksum = parts[0] } return nil } func (mfg *MultiFormatGenerator) validateImage(image *GeneratedImage) error { validator, exists := mfg.validators[image.Format] if !exists { return fmt.Errorf("no validator for format: %s", image.Format) } // Run validation commands for _, command := range validator.Commands { result := mfg.runValidationCommand(image, command) image.ValidationResults = append(image.ValidationResults, result) if result.Status == "failed" { mfg.logger.Warnf("Validation command %s failed: %s", command, result.Message) } } return nil } func (mfg *MultiFormatGenerator) runValidationCommand(image *GeneratedImage, command string) ValidationResult { result := ValidationResult{ Validator: command, Status: "success", Message: "Validation passed", Timestamp: time.Now(), Details: make(map[string]interface{}), } // Run command based on type switch command { case "file": result = mfg.runFileCommand(image) case "qemu-img": result = mfg.runQemuImgCommand(image) case "isoinfo": result = mfg.runIsoInfoCommand(image) default: result.Status = "skipped" result.Message = "Command not implemented" } return result } func (mfg *MultiFormatGenerator) runFileCommand(image *GeneratedImage) ValidationResult { result := ValidationResult{ Validator: "file", Status: "success", Message: "File type validation passed", Timestamp: time.Now(), Details: make(map[string]interface{}), } cmd := exec.Command("file", image.Path) output, err := cmd.Output() if err != nil { result.Status = "failed" result.Message = fmt.Sprintf("file command failed: %v", err) return result } result.Details["file_type"] = strings.TrimSpace(string(output)) return result } func (mfg *MultiFormatGenerator) runQemuImgCommand(image *GeneratedImage) ValidationResult { result := ValidationResult{ Validator: "qemu-img", Status: "success", Message: "QEMU image validation passed", Timestamp: time.Now(), Details: make(map[string]interface{}), } cmd := exec.Command("qemu-img", "info", image.Path) output, err := cmd.Output() if err != nil { result.Status = "failed" result.Message = fmt.Sprintf("qemu-img info failed: %v", err) return result } result.Details["qemu_info"] = strings.TrimSpace(string(output)) return result } func (mfg *MultiFormatGenerator) runIsoInfoCommand(image *GeneratedImage) ValidationResult { result := ValidationResult{ Validator: "isoinfo", Status: "success", Message: "ISO info validation passed", Timestamp: time.Now(), Details: make(map[string]interface{}), } cmd := exec.Command("isoinfo", "-d", "-i", image.Path) output, err := cmd.Output() if err != nil { result.Status = "failed" result.Message = fmt.Sprintf("isoinfo failed: %v", err) return result } result.Details["iso_info"] = strings.TrimSpace(string(output)) return result } func (mfg *MultiFormatGenerator) ListSupportedFormats() []ImageFormat { var formats []ImageFormat for _, format := range mfg.formats { formats = append(formats, format) } return formats } func (mfg *MultiFormatGenerator) GetFormatInfo(format string) (*ImageFormat, error) { imageFormat, exists := mfg.formats[format] if !exists { return nil, fmt.Errorf("unsupported format: %s", format) } return &imageFormat, nil } // Helper functions func generateImageID() string { return fmt.Sprintf("img-%d", time.Now().UnixNano()) } // Blueprint types (imported from blueprintapi) type Blueprint struct { Name string `json:"name"` Packages BlueprintPackages `json:"packages"` Users []BlueprintUser `json:"users"` Customizations BlueprintCustomizations `json:"customizations"` } type BlueprintPackages struct { Include []string `json:"include"` } type BlueprintUser struct { Name string `json:"name"` Shell string `json:"shell"` } type BlueprintCustomizations struct { Hostname string `json:"hostname"` Timezone string `json:"timezone"` }