diff --git a/builder/virtualbox/common/step_export.go b/builder/virtualbox/common/step_export.go index a9f51676..66c16deb 100644 --- a/builder/virtualbox/common/step_export.go +++ b/builder/virtualbox/common/step_export.go @@ -6,6 +6,7 @@ package common import ( "context" "fmt" + "os" "path/filepath" "strings" @@ -14,12 +15,9 @@ import ( ) // This step cleans up forwarded ports and exports the VM to an OVF. -// -// Uses: -// -// Produces: -// -// exportPath string - The path to the resulting export. +// When DiskFormat is set to a non-VMDK format (e.g. VDI), the exported +// VMDK is converted to the requested format after export, and the OVF +// references are updated accordingly. type StepExport struct { Format string OutputDir string @@ -28,6 +26,7 @@ type StepExport struct { Bundling VBoxBundleConfig SkipNatMapping bool SkipExport bool + DiskFormat string } func (s *StepExport) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { @@ -89,9 +88,82 @@ func (s *StepExport) Run(ctx context.Context, state multistep.StateBag) multiste return multistep.ActionHalt } + // If a non-VMDK disk format is requested, convert the exported VMDK + // to the requested format and update the OVF references. + diskFormat := strings.ToUpper(s.DiskFormat) + if diskFormat != "" && diskFormat != "VMDK" { + if err := s.convertExportedDisk(driver, ui, diskFormat); err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + state.Put("exportPath", outputPath) return multistep.ActionContinue } +// convertExportedDisk converts the VMDK produced by VBoxManage export +// to the requested disk format (VDI, VHD) and updates the OVF file. +func (s *StepExport) convertExportedDisk(driver Driver, ui packersdk.Ui, diskFormat string) error { + ext := strings.ToLower(diskFormat) + + // Step A: Find the VMDK file in the output directory + pattern := filepath.Join(s.OutputDir, "*.vmdk") + matches, _ := filepath.Glob(pattern) + if len(matches) == 0 { + return fmt.Errorf("No VMDK file found in %s after export", s.OutputDir) + } + vmdkPath := matches[0] + vmdkBasename := filepath.Base(vmdkPath) + + // Step B: Build the target disk path + vdiBasename := strings.TrimSuffix(vmdkBasename, ".vmdk") + "." + ext + vdiPath := filepath.Join(s.OutputDir, vdiBasename) + + ui.Say(fmt.Sprintf("Converting exported disk from VMDK to %s...", diskFormat)) + ui.Message(fmt.Sprintf(" %s -> %s", vmdkBasename, vdiBasename)) + + // Step C: Convert VMDK to requested format using VBoxManage clonemedium + err := driver.VBoxManage("clonemedium", "disk", vmdkPath, vdiPath, "--format", diskFormat) + if err != nil { + return fmt.Errorf("Error converting disk to %s: %s", diskFormat, err) + } + + // Step D: Delete the VMDK + if err := os.Remove(vmdkPath); err != nil { + ui.Message(fmt.Sprintf("Warning: could not delete VMDK: %s", err)) + } + + // Step E: Read the OVF file + ovfPath := filepath.Join(s.OutputDir, s.OutputFilename+"."+s.Format) + ovfData, err := os.ReadFile(ovfPath) + if err != nil { + return fmt.Errorf("Error reading OVF file: %s", err) + } + + // Step F: Replace VMDK references in OVF + // There are exactly 2 references: + // 1. File href: ovf:href="...disk001.vmdk" + // 2. Disk format URL: ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" + ovfContent := string(ovfData) + ovfContent = strings.ReplaceAll(ovfContent, vmdkBasename, vdiBasename) + ovfContent = strings.ReplaceAll(ovfContent, + "http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized", + "http://www.virtualbox.org/VirtualBox/ExtPack/"+diskFormat) + // Also handle the OVF 0.9 sparse format URL + ovfContent = strings.ReplaceAll(ovfContent, + "http://www.vmware.com/specifications/vmdk.html#sparse", + "http://www.virtualbox.org/VirtualBox/ExtPack/"+diskFormat) + + // Step G: Write the OVF file back + if err := os.WriteFile(ovfPath, []byte(ovfContent), 0644); err != nil { + return fmt.Errorf("Error writing updated OVF file: %s", err) + } + + ui.Say(fmt.Sprintf("Disk converted to %s successfully.", diskFormat)) + return nil +} + func (s *StepExport) Cleanup(state multistep.StateBag) {} diff --git a/builder/virtualbox/iso/builder.go b/builder/virtualbox/iso/builder.go index fee5e7ee..9bce19c9 100644 --- a/builder/virtualbox/iso/builder.go +++ b/builder/virtualbox/iso/builder.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "regexp" + "strings" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/packer-plugin-sdk/bootcommand" @@ -105,6 +106,10 @@ type Config struct { // ostypes. Setting the correct value hints to VirtualBox how to optimize // the virtual hardware to work best with that operating system. GuestOSType string `mapstructure:"guest_os_type" required:"false"` + // The format of the virtual disk to create. Valid values are VDI, VMDK, + // and VHD. Defaults to VDI. VDI supports discard/compact operations. + // VMDK is the OVF standard. VHD is used by Hyper-V. + DiskFormat string `mapstructure:"disk_format" required:"false"` // When this value is set to true, a VDI image will be shrunk in response // to the trim command from the guest OS. The size of the cleared area must // be at least 1MB. Also set hard_drive_nonrotational to true to enable @@ -257,6 +262,16 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { b.config.DiskSize = 40000 } + if b.config.DiskFormat == "" { + b.config.DiskFormat = "VDI" + } + b.config.DiskFormat = strings.ToUpper(b.config.DiskFormat) + switch b.config.DiskFormat { + case "VDI", "VMDK", "VHD": + default: + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("invalid disk_format '%s', must be VDI, VMDK, or VHD", b.config.DiskFormat)) + } + if b.config.HardDriveInterface == "" { b.config.HardDriveInterface = "ide" } @@ -492,6 +507,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) Bundling: b.config.VBoxBundleConfig, SkipNatMapping: b.config.SkipNatMapping, SkipExport: b.config.SkipExport, + DiskFormat: b.config.DiskFormat, }, } diff --git a/builder/virtualbox/iso/builder.hcl2spec.go b/builder/virtualbox/iso/builder.hcl2spec.go index 471630cd..455b48cf 100644 --- a/builder/virtualbox/iso/builder.hcl2spec.go +++ b/builder/virtualbox/iso/builder.hcl2spec.go @@ -133,6 +133,7 @@ type FlatConfig struct { GfxAccelerate3D *bool `mapstructure:"gfx_accelerate_3d" required:"false" cty:"gfx_accelerate_3d" hcl:"gfx_accelerate_3d"` GfxEFIResolution *string `mapstructure:"gfx_efi_resolution" required:"false" cty:"gfx_efi_resolution" hcl:"gfx_efi_resolution"` GuestOSType *string `mapstructure:"guest_os_type" required:"false" cty:"guest_os_type" hcl:"guest_os_type"` + DiskFormat *string `mapstructure:"disk_format" required:"false" cty:"disk_format" hcl:"disk_format"` HardDriveDiscard *bool `mapstructure:"hard_drive_discard" required:"false" cty:"hard_drive_discard" hcl:"hard_drive_discard"` HardDriveInterface *string `mapstructure:"hard_drive_interface" required:"false" cty:"hard_drive_interface" hcl:"hard_drive_interface"` SATAPortCount *int `mapstructure:"sata_port_count" required:"false" cty:"sata_port_count" hcl:"sata_port_count"` @@ -280,6 +281,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "gfx_accelerate_3d": &hcldec.AttrSpec{Name: "gfx_accelerate_3d", Type: cty.Bool, Required: false}, "gfx_efi_resolution": &hcldec.AttrSpec{Name: "gfx_efi_resolution", Type: cty.String, Required: false}, "guest_os_type": &hcldec.AttrSpec{Name: "guest_os_type", Type: cty.String, Required: false}, + "disk_format": &hcldec.AttrSpec{Name: "disk_format", Type: cty.String, Required: false}, "hard_drive_discard": &hcldec.AttrSpec{Name: "hard_drive_discard", Type: cty.Bool, Required: false}, "hard_drive_interface": &hcldec.AttrSpec{Name: "hard_drive_interface", Type: cty.String, Required: false}, "sata_port_count": &hcldec.AttrSpec{Name: "sata_port_count", Type: cty.Number, Required: false}, diff --git a/builder/virtualbox/iso/step_create_disk.go b/builder/virtualbox/iso/step_create_disk.go index 19c9a7e7..812f88eb 100644 --- a/builder/virtualbox/iso/step_create_disk.go +++ b/builder/virtualbox/iso/step_create_disk.go @@ -25,7 +25,7 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult driver := state.Get("driver").(vboxcommon.Driver) ui := state.Get("ui").(packersdk.Ui) vmName := state.Get("vmName").(string) - format := "VDI" + format := config.DiskFormat // The main disk and additional disks diskFullPaths := []string{}