debian-forge-composer/internal/imageformats/multi_format_generator.go
robojerk 4eeaa43c39
Some checks failed
Tests / 🛃 Unit tests (push) Failing after 13s
Tests / 🗄 DB tests (push) Failing after 19s
Tests / 🐍 Lint python scripts (push) Failing after 1s
Tests / ⌨ Golang Lint (push) Failing after 1s
Tests / 📦 Packit config lint (push) Failing after 1s
Tests / 🔍 Check source preparation (push) Failing after 1s
Tests / 🔍 Check for valid snapshot urls (push) Failing after 1s
Tests / 🔍 Check for missing or unused runner repos (push) Failing after 1s
Tests / 🐚 Shellcheck (push) Failing after 1s
Tests / 📦 RPMlint (push) Failing after 1s
Tests / Gitlab CI trigger helper (push) Failing after 1s
Tests / 🎀 kube-linter (push) Failing after 1s
Tests / 🧹 cloud-cleaner-is-enabled (push) Successful in 3s
Tests / 🔍 Check spec file osbuild/images dependencies (push) Failing after 1s
did stuff
2025-08-26 10:34:42 -07:00

677 lines
18 KiB
Go

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"`
}