Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 78 additions & 6 deletions builder/virtualbox/common/step_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package common
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {}
16 changes: 16 additions & 0 deletions builder/virtualbox/iso/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"regexp"
"strings"

"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/bootcommand"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -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,
},
}

Expand Down
2 changes: 2 additions & 0 deletions builder/virtualbox/iso/builder.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion builder/virtualbox/iso/step_create_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down