diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index 0265fcad5..05c71e2c6 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -68,7 +68,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack Files: b.config.CDConfig.CDFiles, Label: b.config.CDConfig.CDLabel, }, - new(stepCreateDisk), + &stepCreateDisk{ + AdditionalDiskSize: b.config.AdditionalDiskSize, + DiskImage: b.config.DiskImage, + DiskSize: b.config.DiskSize, + Format: b.config.Format, + OutputDir: b.config.OutputDir, + UseBackingFile: b.config.UseBackingFile, + VMName: b.config.VMName, + QemuImgArgs: b.config.QemuImgArgs, + }, &stepCopyDisk{ DiskImage: b.config.DiskImage, Format: b.config.Format, @@ -76,7 +85,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack UseBackingFile: b.config.UseBackingFile, VMName: b.config.VMName, }, - new(stepResizeDisk), + &stepResizeDisk{ + DiskCompression: b.config.DiskCompression, + DiskImage: b.config.DiskImage, + Format: b.config.Format, + OutputDir: b.config.OutputDir, + SkipResizeDisk: b.config.SkipResizeDisk, + VMName: b.config.VMName, + DiskSize: b.config.DiskSize, + QemuImgArgs: b.config.QemuImgArgs, + }, new(stepHTTPIPDiscover), &common.StepHTTPServer{ HTTPDir: b.config.HTTPDir, @@ -113,7 +131,13 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack Comm: &b.config.CommConfig.Comm, }, new(stepShutdown), - new(stepConvertDisk), + &stepConvertDisk{ + DiskCompression: b.config.DiskCompression, + Format: b.config.Format, + OutputDir: b.config.OutputDir, + SkipCompaction: b.config.SkipCompaction, + VMName: b.config.VMName, + }, ) // Setup the state bag diff --git a/builder/qemu/config.go b/builder/qemu/config.go index e095a2bcd..826b2edef 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -1,5 +1,5 @@ //go:generate struct-markdown -//go:generate mapstructure-to-hcl2 -type Config +//go:generate mapstructure-to-hcl2 -type Config,QemuImgArgs package qemu @@ -57,6 +57,12 @@ var diskDZeroes = map[string]bool{ "off": true, } +type QemuImgArgs struct { + Convert []string `mapstructure:"convert" required:"false"` + Create []string `mapstructure:"create" required:"false"` + Resize []string `mapstructure:"resize" required:"false"` +} + type Config struct { common.PackerConfig `mapstructure:",squash"` common.HTTPConfig `mapstructure:",squash"` @@ -297,6 +303,36 @@ type Config struct { // `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`, // `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}` QemuArgs [][]string `mapstructure:"qemuargs" required:"false"` + // A map of custom arguments to pass to qemu-img commands, where the key + // is the subcommand, and the values are lists of strings for each flag. + // Example: + // + // In JSON: + // ```json + // { + // "qemu_img_args": { + // "convert": ["-o", "preallocation=full"], + // "resize": ["-foo", "bar"] + // } + // ``` + // Please note + // that unlike qemuargs, these commands are not split into switch-value + // sub-arrays, because the basic elements in qemu-img calls are unlikely + // to need an actual override. + // The arguments will be constructed as follows: + // - Convert: + // Default is `qemu-img convert -O $format $sourcepath $targetpath`. Adding + // arguments ["-foo", "bar"] to qemu_img_args.convert will change this to + // `qemu-img convert -foo bar -O $format $sourcepath $targetpath` + // - Create: + // Default is `create -f $format $targetpath $size`. Adding arguments + // ["-foo", "bar"] to qemu_img_args.create will change this to + // "create -f qcow2 -foo bar target.qcow2 1234M" + // - Resize: + // Default is `qemu-img resize -f $format $sourcepath $size`. Adding + // arguments ["-foo", "bar"] to qemu_img_args.resize will change this to + // `qemu-img resize -f $format -foo bar $sourcepath $size` + QemuImgArgs QemuImgArgs `mapstructure:"qemu_img_args" required:"false"` // The name of the Qemu binary to look for. This // defaults to qemu-system-x86_64, but may need to be changed for // some platforms. For example qemu-kvm, or qemu-system-i386 may be a diff --git a/builder/qemu/config.hcl2spec.go b/builder/qemu/config.hcl2spec.go index 9557040d9..cda5b683e 100644 --- a/builder/qemu/config.hcl2spec.go +++ b/builder/qemu/config.hcl2spec.go @@ -1,4 +1,4 @@ -// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT. +// Code generated by "mapstructure-to-hcl2 -type Config,QemuImgArgs"; DO NOT EDIT. package qemu import ( @@ -112,6 +112,7 @@ type FlatConfig struct { NetBridge *string `mapstructure:"net_bridge" required:"false" cty:"net_bridge" hcl:"net_bridge"` OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"` QemuArgs [][]string `mapstructure:"qemuargs" required:"false" cty:"qemuargs" hcl:"qemuargs"` + QemuImgArgs *FlatQemuImgArgs `mapstructure:"qemu_img_args" required:"false" cty:"qemu_img_args" hcl:"qemu_img_args"` QemuBinary *string `mapstructure:"qemu_binary" required:"false" cty:"qemu_binary" hcl:"qemu_binary"` QMPEnable *bool `mapstructure:"qmp_enable" required:"false" cty:"qmp_enable" hcl:"qmp_enable"` QMPSocketPath *string `mapstructure:"qmp_socket_path" required:"false" cty:"qmp_socket_path" hcl:"qmp_socket_path"` @@ -241,6 +242,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "net_bridge": &hcldec.AttrSpec{Name: "net_bridge", Type: cty.String, Required: false}, "output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false}, "qemuargs": &hcldec.AttrSpec{Name: "qemuargs", Type: cty.List(cty.List(cty.String)), Required: false}, + "qemu_img_args": &hcldec.BlockSpec{TypeName: "qemu_img_args", Nested: hcldec.ObjectSpec((*FlatQemuImgArgs)(nil).HCL2Spec())}, "qemu_binary": &hcldec.AttrSpec{Name: "qemu_binary", Type: cty.String, Required: false}, "qmp_enable": &hcldec.AttrSpec{Name: "qmp_enable", Type: cty.Bool, Required: false}, "qmp_socket_path": &hcldec.AttrSpec{Name: "qmp_socket_path", Type: cty.String, Required: false}, @@ -256,3 +258,30 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { } return s } + +// FlatQemuImgArgs is an auto-generated flat version of QemuImgArgs. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatQemuImgArgs struct { + Convert []string `mapstructure:"convert" required:"false" cty:"convert" hcl:"convert"` + Create []string `mapstructure:"create" required:"false" cty:"create" hcl:"create"` + Resize []string `mapstructure:"resize" required:"false" cty:"resize" hcl:"resize"` +} + +// FlatMapstructure returns a new FlatQemuImgArgs. +// FlatQemuImgArgs is an auto-generated flat version of QemuImgArgs. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*QemuImgArgs) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatQemuImgArgs) +} + +// HCL2Spec returns the hcl spec of a QemuImgArgs. +// This spec is used by HCL to read the fields of QemuImgArgs. +// The decoded values from this spec will then be applied to a FlatQemuImgArgs. +func (*FlatQemuImgArgs) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "convert": &hcldec.AttrSpec{Name: "convert", Type: cty.List(cty.String), Required: false}, + "create": &hcldec.AttrSpec{Name: "create", Type: cty.List(cty.String), Required: false}, + "resize": &hcldec.AttrSpec{Name: "resize", Type: cty.List(cty.String), Required: false}, + } + return s +} diff --git a/builder/qemu/config_test.go b/builder/qemu/config_test.go index 67cb3e13a..bd94d6eb8 100644 --- a/builder/qemu/config_test.go +++ b/builder/qemu/config_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/hashicorp/packer/packer" + "github.com/stretchr/testify/assert" ) var testPem = ` @@ -668,3 +669,26 @@ func TestCommConfigPrepare_BackwardsCompatibility(t *testing.T) { t.Fatalf("HostPortMax should be %d for backwards compatibility, but it was %d", hostPortMax, c.CommConfig.HostPortMax) } } + +func TestBuilderPrepare_LoadQemuImgArgs(t *testing.T) { + var c Config + config := testConfig() + config["qemu_img_args"] = map[string][]string{ + "convert": []string{"-o", "preallocation=full"}, + "resize": []string{"-foo", "bar"}, + "create": []string{"-baz", "bang"}, + } + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + assert.Equal(t, []string{"-o", "preallocation=full"}, + c.QemuImgArgs.Convert, "Convert args not loaded properly") + assert.Equal(t, []string{"-foo", "bar"}, + c.QemuImgArgs.Resize, "Resize args not loaded properly") + assert.Equal(t, []string{"-baz", "bang"}, + c.QemuImgArgs.Create, "Create args not loaded properly") +} diff --git a/builder/qemu/driver_mock.go b/builder/qemu/driver_mock.go index 382892866..7fa1a7df7 100644 --- a/builder/qemu/driver_mock.go +++ b/builder/qemu/driver_mock.go @@ -18,7 +18,7 @@ type DriverMock struct { WaitForShutdownState bool QemuImgCalled bool - QemuImgCalls [][]string + QemuImgCalls []string QemuImgErrs []error VerifyCalled bool @@ -55,7 +55,7 @@ func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool { func (d *DriverMock) QemuImg(args ...string) error { d.QemuImgCalled = true - d.QemuImgCalls = append(d.QemuImgCalls, args) + d.QemuImgCalls = append(d.QemuImgCalls, args...) if len(d.QemuImgErrs) >= len(d.QemuImgCalls) { return d.QemuImgErrs[len(d.QemuImgCalls)-1] diff --git a/builder/qemu/step_convert_disk.go b/builder/qemu/step_convert_disk.go index 249efb414..8e06cc821 100644 --- a/builder/qemu/step_convert_disk.go +++ b/builder/qemu/step_convert_disk.go @@ -17,37 +17,32 @@ import ( // This step converts the virtual disk that was used as the // hard drive for the virtual machine. -type stepConvertDisk struct{} +type stepConvertDisk struct { + DiskCompression bool + Format string + OutputDir string + SkipCompaction bool + VMName string + + QemuImgArgs QemuImgArgs +} func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - config := state.Get("config").(*Config) driver := state.Get("driver").(Driver) - diskName := config.VMName ui := state.Get("ui").(packer.Ui) - if config.SkipCompaction && !config.DiskCompression { + diskName := s.VMName + + if s.SkipCompaction && !s.DiskCompression { return multistep.ActionContinue } name := diskName + ".convert" - sourcePath := filepath.Join(config.OutputDir, diskName) - targetPath := filepath.Join(config.OutputDir, name) + sourcePath := filepath.Join(s.OutputDir, diskName) + targetPath := filepath.Join(s.OutputDir, name) - command := []string{ - "convert", - } - - if config.DiskCompression { - command = append(command, "-c") - } - - command = append(command, []string{ - "-O", config.Format, - sourcePath, - targetPath, - }..., - ) + command := s.buildConvertCommand(sourcePath, targetPath) ui.Say("Converting hard drive...") // Retry the conversion a few times in case it takes the qemu process a @@ -90,4 +85,20 @@ func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) mul return multistep.ActionContinue } +func (s *stepConvertDisk) buildConvertCommand(sourcePath, targetPath string) []string { + command := []string{"convert"} + + if s.DiskCompression { + command = append(command, "-c") + } + + // Add user-provided convert args + command = append(command, s.QemuImgArgs.Convert...) + + // Add format, and paths. + command = append(command, "-O", s.Format, sourcePath, targetPath) + + return command +} + func (s *stepConvertDisk) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_convert_disk_test.go b/builder/qemu/step_convert_disk_test.go new file mode 100644 index 000000000..e6ff4b0c4 --- /dev/null +++ b/builder/qemu/step_convert_disk_test.go @@ -0,0 +1,52 @@ +package qemu + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_buildConvertCommand(t *testing.T) { + type testCase struct { + Step *stepConvertDisk + Expected []string + Reason string + } + testcases := []testCase{ + { + &stepConvertDisk{ + Format: "qcow2", + DiskCompression: false, + }, + []string{"convert", "-O", "qcow2", "source.qcow", "target.qcow2"}, + "Basic, happy path, no compression, no extra args", + }, + { + &stepConvertDisk{ + Format: "qcow2", + DiskCompression: true, + }, + []string{"convert", "-c", "-O", "qcow2", "source.qcow", "target.qcow2"}, + "Basic, happy path, with compression, no extra args", + }, + { + &stepConvertDisk{ + Format: "qcow2", + DiskCompression: true, + QemuImgArgs: QemuImgArgs{ + Convert: []string{"-o", "preallocation=full"}, + }, + }, + []string{"convert", "-c", "-o", "preallocation=full", "-O", "qcow2", "source.qcow", "target.qcow2"}, + "Basic, happy path, with compression, one set of extra args", + }, + } + + for _, tc := range testcases { + command := tc.Step.buildConvertCommand("source.qcow", "target.qcow2") + + assert.Equal(t, command, tc.Expected, + fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected)) + } +} diff --git a/builder/qemu/step_copy_disk.go b/builder/qemu/step_copy_disk.go index 5d461104d..28a091426 100644 --- a/builder/qemu/step_copy_disk.go +++ b/builder/qemu/step_copy_disk.go @@ -17,6 +17,8 @@ type stepCopyDisk struct { OutputDir string UseBackingFile bool VMName string + + QemuImgArgs QemuImgArgs } func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { @@ -43,12 +45,7 @@ func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multis return multistep.ActionContinue } - command := []string{ - "convert", - "-O", s.Format, - isoPath, - path, - } + command := s.buildConvertCommand(isoPath, path) ui.Say("Copying hard drive...") if err := driver.QemuImg(command...); err != nil { @@ -61,4 +58,16 @@ func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multis return multistep.ActionContinue } +func (s *stepCopyDisk) buildConvertCommand(sourcePath, targetPath string) []string { + command := []string{"convert"} + + // Add user-provided convert args + command = append(command, s.QemuImgArgs.Convert...) + + // Add format, and paths. + command = append(command, "-O", s.Format, sourcePath, targetPath) + + return command +} + func (s *stepCopyDisk) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_copy_disk_test.go b/builder/qemu/step_copy_disk_test.go index efe30f4ea..b7241621e 100644 --- a/builder/qemu/step_copy_disk_test.go +++ b/builder/qemu/step_copy_disk_test.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" + "github.com/stretchr/testify/assert" ) func copyTestState(t *testing.T, d *DriverMock) multistep.StateBag { @@ -89,3 +90,33 @@ func Test_StepQemuImgCalled(t *testing.T) { t.Fatalf("Should have called qemu-img since extensions don't match") } } + +func Test_StepQemuImgCalledWithExtraArgs(t *testing.T) { + step := &stepCopyDisk{ + DiskImage: true, + Format: "raw", + VMName: "output.qcow2", + QemuImgArgs: QemuImgArgs{ + Convert: []string{"-o", "preallocation=full"}, + }, + } + + d := new(DriverMock) + state := copyTestState(t, d) + action := step.Run(context.TODO(), state) + if action != multistep.ActionContinue { + t.Fatalf("Should have gotten an ActionContinue") + } + if d.CopyCalled { + t.Fatalf("Should not have copied since extensions don't match") + } + if !d.QemuImgCalled { + t.Fatalf("Should have called qemu-img since extensions don't match") + } + assert.Equal( + t, + d.QemuImgCalls, + []string{"convert", "-o", "preallocation=full", "-O", "raw", + "example_source.qcow2", "output.qcow2"}, + "should have added user extra args") +} diff --git a/builder/qemu/step_create_disk.go b/builder/qemu/step_create_disk.go index cc904f8fc..288bb02dc 100644 --- a/builder/qemu/step_create_disk.go +++ b/builder/qemu/step_create_disk.go @@ -12,15 +12,23 @@ import ( // This step creates the virtual disk that will be used as the // hard drive for the virtual machine. -type stepCreateDisk struct{} +type stepCreateDisk struct { + AdditionalDiskSize []string + DiskImage bool + DiskSize string + Format string + OutputDir string + UseBackingFile bool + VMName string + QemuImgArgs QemuImgArgs +} func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - config := state.Get("config").(*Config) driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) - name := config.VMName + name := s.VMName - if config.DiskImage && !config.UseBackingFile { + if s.DiskImage && !s.UseBackingFile { return multistep.ActionContinue } @@ -28,12 +36,12 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult ui.Say("Creating required virtual machine disks") // The 'main' or 'default' disk - diskFullPaths = append(diskFullPaths, filepath.Join(config.OutputDir, name)) - diskSizes = append(diskSizes, config.DiskSize) + diskFullPaths = append(diskFullPaths, filepath.Join(s.OutputDir, name)) + diskSizes = append(diskSizes, s.DiskSize) // Additional disks - if len(config.AdditionalDiskSize) > 0 { - for i, diskSize := range config.AdditionalDiskSize { - path := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d", name, i+1)) + if len(s.AdditionalDiskSize) > 0 { + for i, diskSize := range s.AdditionalDiskSize { + path := filepath.Join(s.OutputDir, fmt.Sprintf("%s-%d", name, i+1)) diskFullPaths = append(diskFullPaths, path) size := diskSize diskSizes = append(diskSizes, size) @@ -43,19 +51,8 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult // Create all required disks for i, diskFullPath := range diskFullPaths { log.Printf("[INFO] Creating disk with Path: %s and Size: %s", diskFullPath, diskSizes[i]) - command := []string{ - "create", - "-f", config.Format, - } - if config.UseBackingFile && i == 0 { - isoPath := state.Get("iso_path").(string) - command = append(command, "-b", isoPath) - } - - command = append(command, - diskFullPath, - diskSizes[i]) + command := s.buildCreateCommand(diskFullPath, diskSizes[i], i, state) if err := driver.QemuImg(command...); err != nil { err := fmt.Errorf("Error creating hard drive: %s", err) @@ -71,4 +68,21 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult return multistep.ActionContinue } +func (s *stepCreateDisk) buildCreateCommand(path string, size string, i int, state multistep.StateBag) []string { + command := []string{"create", "-f", s.Format} + + if s.UseBackingFile && i == 0 { + isoPath := state.Get("iso_path").(string) + command = append(command, "-b", isoPath) + } + + // add user-provided convert args + command = append(command, s.QemuImgArgs.Create...) + + // add target path and size. + command = append(command, path, size) + + return command +} + func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_create_disk_test.go b/builder/qemu/step_create_disk_test.go new file mode 100644 index 000000000..22fa59283 --- /dev/null +++ b/builder/qemu/step_create_disk_test.go @@ -0,0 +1,80 @@ +package qemu + +import ( + "fmt" + "testing" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/stretchr/testify/assert" +) + +func Test_buildCreateCommand(t *testing.T) { + type testCase struct { + Step *stepCreateDisk + I int + Expected []string + Reason string + } + testcases := []testCase{ + { + &stepCreateDisk{ + Format: "qcow2", + UseBackingFile: false, + }, + 0, + []string{"create", "-f", "qcow2", "target.qcow2", "1234M"}, + "Basic, happy path, no backing store, no extra args", + }, + { + &stepCreateDisk{ + Format: "qcow2", + UseBackingFile: true, + }, + 0, + []string{"create", "-f", "qcow2", "-b", "source.qcow2", "target.qcow2", "1234M"}, + "Basic, happy path, backing store, no extra args", + }, + { + &stepCreateDisk{ + Format: "qcow2", + UseBackingFile: true, + }, + 1, + []string{"create", "-f", "qcow2", "target.qcow2", "1234M"}, + "Basic, happy path, backing store set but not at first index, no extra args", + }, + { + &stepCreateDisk{ + Format: "qcow2", + UseBackingFile: true, + QemuImgArgs: QemuImgArgs{ + Create: []string{"-foo", "bar"}, + }, + }, + 0, + []string{"create", "-f", "qcow2", "-b", "source.qcow2", "-foo", "bar", "target.qcow2", "1234M"}, + "Basic, happy path, backing store set, extra args", + }, + { + &stepCreateDisk{ + Format: "qcow2", + UseBackingFile: true, + QemuImgArgs: QemuImgArgs{ + Create: []string{"-foo", "bar"}, + }, + }, + 1, + []string{"create", "-f", "qcow2", "-foo", "bar", "target.qcow2", "1234M"}, + "Basic, happy path, backing store set but not at first index, extra args", + }, + } + + for _, tc := range testcases { + state := new(multistep.BasicStateBag) + state.Put("iso_path", "source.qcow2") + command := tc.Step.buildCreateCommand("target.qcow2", "1234M", tc.I, state) + + assert.Equal(t, command, tc.Expected, + fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected)) + } +} diff --git a/builder/qemu/step_resize_disk.go b/builder/qemu/step_resize_disk.go index d79768214..09863e327 100644 --- a/builder/qemu/step_resize_disk.go +++ b/builder/qemu/step_resize_disk.go @@ -11,22 +11,26 @@ import ( // This step resizes the virtual disk that will be used as the // hard drive for the virtual machine. -type stepResizeDisk struct{} +type stepResizeDisk struct { + DiskCompression bool + DiskImage bool + Format string + OutputDir string + SkipResizeDisk bool + VMName string + DiskSize string + + QemuImgArgs QemuImgArgs +} func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - config := state.Get("config").(*Config) driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) - path := filepath.Join(config.OutputDir, config.VMName) + path := filepath.Join(s.OutputDir, s.VMName) - command := []string{ - "resize", - "-f", config.Format, - path, - config.DiskSize, - } + command := s.buildResizeCommand(path) - if config.DiskImage == false || config.SkipResizeDisk == true { + if s.DiskImage == false || s.SkipResizeDisk == true { return multistep.ActionContinue } @@ -41,4 +45,16 @@ func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) mult return multistep.ActionContinue } +func (s *stepResizeDisk) buildResizeCommand(path string) []string { + command := []string{"resize", "-f", s.Format} + + // add user-provided convert args + command = append(command, s.QemuImgArgs.Resize...) + + // Add file and size + command = append(command, path, s.DiskSize) + + return command +} + func (s *stepResizeDisk) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_resize_disk_test.go b/builder/qemu/step_resize_disk_test.go index a4335d54e..796ec20cb 100644 --- a/builder/qemu/step_resize_disk_test.go +++ b/builder/qemu/step_resize_disk_test.go @@ -2,81 +2,76 @@ package qemu import ( "context" + "fmt" "testing" "github.com/hashicorp/packer/helper/multistep" + "github.com/stretchr/testify/assert" ) -func TestStepResizeDisk_Run(t *testing.T) { - state := testState(t) - driver := state.Get("driver").(*DriverMock) +func TestStepResizeDisk_Skips(t *testing.T) { + testConfigs := []*Config{ + &Config{ + DiskImage: false, + SkipResizeDisk: false, + }, + &Config{ + DiskImage: false, + SkipResizeDisk: true, + }, + } + for _, config := range testConfigs { + state := testState(t) + driver := state.Get("driver").(*DriverMock) - config := &Config{ - DiskImage: true, - SkipResizeDisk: false, - DiskSize: "4096M", - Format: "qcow2", - OutputDir: "/test/", - VMName: "test", - } - state.Put("config", config) - step := new(stepResizeDisk) + state.Put("config", config) + step := new(stepResizeDisk) - // Test the run - if action := step.Run(context.Background(), state); action != multistep.ActionContinue { - t.Fatalf("bad action: %#v", action) - } - if _, ok := state.GetOk("error"); ok { - t.Fatal("should NOT have error") - } - if len(driver.QemuImgCalls) == 0 { - t.Fatal("should qemu-img called") - } - if len(driver.QemuImgCalls[0]) != 5 { - t.Fatal("should 5 qemu-img parameters") + // Test the run + if action := step.Run(context.Background(), state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + if _, ok := state.GetOk("error"); ok { + t.Fatal("should NOT have error") + } + if len(driver.QemuImgCalls) > 0 { + t.Fatal("should NOT have called qemu-img") + } } } -func TestStepResizeDisk_SkipIso(t *testing.T) { - state := testState(t) - driver := state.Get("driver").(*DriverMock) - config := &Config{ - DiskImage: false, - SkipResizeDisk: false, +func Test_buildResizeCommand(t *testing.T) { + type testCase struct { + Step *stepResizeDisk + Expected []string + Reason string + } + testcases := []testCase{ + { + &stepResizeDisk{ + Format: "qcow2", + DiskSize: "1234M", + }, + []string{"resize", "-f", "qcow2", "source.qcow", "1234M"}, + "no extra args", + }, + { + &stepResizeDisk{ + Format: "qcow2", + DiskSize: "1234M", + QemuImgArgs: QemuImgArgs{ + Resize: []string{"-foo", "bar"}, + }, + }, + []string{"resize", "-f", "qcow2", "-foo", "bar", "source.qcow", "1234M"}, + "one set of extra args", + }, } - state.Put("config", config) - step := new(stepResizeDisk) - // Test the run - if action := step.Run(context.Background(), state); action != multistep.ActionContinue { - t.Fatalf("bad action: %#v", action) - } - if _, ok := state.GetOk("error"); ok { - t.Fatal("should NOT have error") - } - if len(driver.QemuImgCalls) > 0 { - t.Fatal("should NOT qemu-img called") - } -} - -func TestStepResizeDisk_SkipOption(t *testing.T) { - state := testState(t) - driver := state.Get("driver").(*DriverMock) - config := &Config{ - DiskImage: false, - SkipResizeDisk: true, - } - state.Put("config", config) - step := new(stepResizeDisk) - - // Test the run - if action := step.Run(context.Background(), state); action != multistep.ActionContinue { - t.Fatalf("bad action: %#v", action) - } - if _, ok := state.GetOk("error"); ok { - t.Fatal("should NOT have error") - } - if len(driver.QemuImgCalls) > 0 { - t.Fatal("should NOT qemu-img called") + for _, tc := range testcases { + command := tc.Step.buildResizeCommand("source.qcow") + + assert.Equal(t, command, tc.Expected, + fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected)) } } diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go index 98b913cbd..d7f60d37f 100644 --- a/builder/qemu/step_run.go +++ b/builder/qemu/step_run.go @@ -247,7 +247,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error inArgs := make(map[string][]string) if len(config.QemuArgs) > 0 { - ui.Say("Overriding defaults Qemu arguments with QemuArgs...") + ui.Say("Overriding default Qemu arguments with QemuArgs...") httpIp := state.Get("http_ip").(string) httpPort := state.Get("http_port").(int) diff --git a/website/pages/partials/builder/qemu/Config-not-required.mdx b/website/pages/partials/builder/qemu/Config-not-required.mdx index e43002c27..9b5b140de 100644 --- a/website/pages/partials/builder/qemu/Config-not-required.mdx +++ b/website/pages/partials/builder/qemu/Config-not-required.mdx @@ -231,6 +231,36 @@ `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`, `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}` +- `qemu_img_args` (QemuImgArgs) - A map of custom arguments to pass to qemu-img commands, where the key + is the subcommand, and the values are lists of strings for each flag. + Example: + + In JSON: + ```json + { + "qemu_img_args": { + "convert": ["-o", "preallocation=full"], + "resize": ["-foo", "bar"] + } + ``` + Please note + that unlike qemuargs, these commands are not split into switch-value + sub-arrays, because the basic elements in qemu-img calls are unlikely + to need an actual override. + The arguments will be constructed as follows: + - Convert: + Default is `qemu-img convert -O $format $sourcepath $targetpath`. Adding + arguments ["-foo", "bar"] to qemu_img_args.convert will change this to + `qemu-img convert -foo bar -O $format $sourcepath $targetpath` + - Create: + Default is `create -f $format $targetpath $size`. Adding arguments + ["-foo", "bar"] to qemu_img_args.create will change this to + "create -f qcow2 -foo bar target.qcow2 1234M" + - Resize: + Default is `qemu-img resize -f $format $sourcepath $size`. Adding + arguments ["-foo", "bar"] to qemu_img_args.resize will change this to + `qemu-img resize -f $format -foo bar $sourcepath $size` + - `qemu_binary` (string) - The name of the Qemu binary to look for. This defaults to qemu-system-x86_64, but may need to be changed for some platforms. For example qemu-kvm, or qemu-system-i386 may be a diff --git a/website/pages/partials/builder/qemu/QemuImgArgs-not-required.mdx b/website/pages/partials/builder/qemu/QemuImgArgs-not-required.mdx new file mode 100644 index 000000000..7507e54a0 --- /dev/null +++ b/website/pages/partials/builder/qemu/QemuImgArgs-not-required.mdx @@ -0,0 +1,7 @@ + + +- `convert` ([]string) - Convert + +- `create` ([]string) - Create + +- `resize` ([]string) - Resize