Merge pull request #9956 from hashicorp/do_6734

builder/qemu: Add qemu_img_args option to set special cli flags for our calls to qemu-img
This commit is contained in:
Megan Marsh 2020-09-18 11:24:34 -07:00 committed by GitHub
commit 77817f80a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 487 additions and 129 deletions

View File

@ -68,7 +68,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Files: b.config.CDConfig.CDFiles, Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel, 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{ &stepCopyDisk{
DiskImage: b.config.DiskImage, DiskImage: b.config.DiskImage,
Format: b.config.Format, 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, UseBackingFile: b.config.UseBackingFile,
VMName: b.config.VMName, 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), new(stepHTTPIPDiscover),
&common.StepHTTPServer{ &common.StepHTTPServer{
HTTPDir: b.config.HTTPDir, 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, Comm: &b.config.CommConfig.Comm,
}, },
new(stepShutdown), 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 // Setup the state bag

View File

@ -1,5 +1,5 @@
//go:generate struct-markdown //go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type Config //go:generate mapstructure-to-hcl2 -type Config,QemuImgArgs
package qemu package qemu
@ -57,6 +57,12 @@ var diskDZeroes = map[string]bool{
"off": true, "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 { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"` common.HTTPConfig `mapstructure:",squash"`
@ -297,6 +303,36 @@ type Config struct {
// `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`, // `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`,
// `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}` // `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false"` 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 // The name of the Qemu binary to look for. This
// defaults to qemu-system-x86_64, but may need to be changed for // 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 // some platforms. For example qemu-kvm, or qemu-system-i386 may be a

View File

@ -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 package qemu
import ( import (
@ -112,6 +112,7 @@ type FlatConfig struct {
NetBridge *string `mapstructure:"net_bridge" required:"false" cty:"net_bridge" hcl:"net_bridge"` 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"` OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false" cty:"qemuargs" hcl:"qemuargs"` 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"` 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"` 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"` 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}, "net_bridge": &hcldec.AttrSpec{Name: "net_bridge", Type: cty.String, Required: false},
"output_directory": &hcldec.AttrSpec{Name: "output_directory", 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}, "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}, "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_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}, "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 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
}

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
) )
var testPem = ` 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) 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")
}

View File

@ -18,7 +18,7 @@ type DriverMock struct {
WaitForShutdownState bool WaitForShutdownState bool
QemuImgCalled bool QemuImgCalled bool
QemuImgCalls [][]string QemuImgCalls []string
QemuImgErrs []error QemuImgErrs []error
VerifyCalled bool VerifyCalled bool
@ -55,7 +55,7 @@ func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool {
func (d *DriverMock) QemuImg(args ...string) error { func (d *DriverMock) QemuImg(args ...string) error {
d.QemuImgCalled = true d.QemuImgCalled = true
d.QemuImgCalls = append(d.QemuImgCalls, args) d.QemuImgCalls = append(d.QemuImgCalls, args...)
if len(d.QemuImgErrs) >= len(d.QemuImgCalls) { if len(d.QemuImgErrs) >= len(d.QemuImgCalls) {
return d.QemuImgErrs[len(d.QemuImgCalls)-1] return d.QemuImgErrs[len(d.QemuImgCalls)-1]

View File

@ -17,37 +17,32 @@ import (
// This step converts the virtual disk that was used as the // This step converts the virtual disk that was used as the
// hard drive for the virtual machine. // 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 { func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
diskName := config.VMName
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
if config.SkipCompaction && !config.DiskCompression { diskName := s.VMName
if s.SkipCompaction && !s.DiskCompression {
return multistep.ActionContinue return multistep.ActionContinue
} }
name := diskName + ".convert" name := diskName + ".convert"
sourcePath := filepath.Join(config.OutputDir, diskName) sourcePath := filepath.Join(s.OutputDir, diskName)
targetPath := filepath.Join(config.OutputDir, name) targetPath := filepath.Join(s.OutputDir, name)
command := []string{ command := s.buildConvertCommand(sourcePath, targetPath)
"convert",
}
if config.DiskCompression {
command = append(command, "-c")
}
command = append(command, []string{
"-O", config.Format,
sourcePath,
targetPath,
}...,
)
ui.Say("Converting hard drive...") ui.Say("Converting hard drive...")
// Retry the conversion a few times in case it takes the qemu process a // 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 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) {} func (s *stepConvertDisk) Cleanup(state multistep.StateBag) {}

View File

@ -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))
}
}

View File

@ -17,6 +17,8 @@ type stepCopyDisk struct {
OutputDir string OutputDir string
UseBackingFile bool UseBackingFile bool
VMName string VMName string
QemuImgArgs QemuImgArgs
} }
func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 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 return multistep.ActionContinue
} }
command := []string{ command := s.buildConvertCommand(isoPath, path)
"convert",
"-O", s.Format,
isoPath,
path,
}
ui.Say("Copying hard drive...") ui.Say("Copying hard drive...")
if err := driver.QemuImg(command...); err != nil { 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 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) {} func (s *stepCopyDisk) Cleanup(state multistep.StateBag) {}

View File

@ -6,6 +6,7 @@ import (
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
) )
func copyTestState(t *testing.T, d *DriverMock) multistep.StateBag { 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") 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")
}

View File

@ -12,15 +12,23 @@ import (
// This step creates the virtual disk that will be used as the // This step creates the virtual disk that will be used as the
// hard drive for the virtual machine. // 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 { func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) 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 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") ui.Say("Creating required virtual machine disks")
// The 'main' or 'default' disk // The 'main' or 'default' disk
diskFullPaths = append(diskFullPaths, filepath.Join(config.OutputDir, name)) diskFullPaths = append(diskFullPaths, filepath.Join(s.OutputDir, name))
diskSizes = append(diskSizes, config.DiskSize) diskSizes = append(diskSizes, s.DiskSize)
// Additional disks // Additional disks
if len(config.AdditionalDiskSize) > 0 { if len(s.AdditionalDiskSize) > 0 {
for i, diskSize := range config.AdditionalDiskSize { for i, diskSize := range s.AdditionalDiskSize {
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d", name, i+1)) path := filepath.Join(s.OutputDir, fmt.Sprintf("%s-%d", name, i+1))
diskFullPaths = append(diskFullPaths, path) diskFullPaths = append(diskFullPaths, path)
size := diskSize size := diskSize
diskSizes = append(diskSizes, size) diskSizes = append(diskSizes, size)
@ -43,19 +51,8 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult
// Create all required disks // Create all required disks
for i, diskFullPath := range diskFullPaths { for i, diskFullPath := range diskFullPaths {
log.Printf("[INFO] Creating disk with Path: %s and Size: %s", diskFullPath, diskSizes[i]) 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 { command := s.buildCreateCommand(diskFullPath, diskSizes[i], i, state)
isoPath := state.Get("iso_path").(string)
command = append(command, "-b", isoPath)
}
command = append(command,
diskFullPath,
diskSizes[i])
if err := driver.QemuImg(command...); err != nil { if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err) 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 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) {} func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {}

View File

@ -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))
}
}

View File

@ -11,22 +11,26 @@ import (
// This step resizes the virtual disk that will be used as the // This step resizes the virtual disk that will be used as the
// hard drive for the virtual machine. // 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 { func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
path := filepath.Join(config.OutputDir, config.VMName) path := filepath.Join(s.OutputDir, s.VMName)
command := []string{ command := s.buildResizeCommand(path)
"resize",
"-f", config.Format,
path,
config.DiskSize,
}
if config.DiskImage == false || config.SkipResizeDisk == true { if s.DiskImage == false || s.SkipResizeDisk == true {
return multistep.ActionContinue return multistep.ActionContinue
} }
@ -41,4 +45,16 @@ func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) mult
return multistep.ActionContinue 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) {} func (s *stepResizeDisk) Cleanup(state multistep.StateBag) {}

View File

@ -2,81 +2,76 @@ package qemu
import ( import (
"context" "context"
"fmt"
"testing" "testing"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
"github.com/stretchr/testify/assert"
) )
func TestStepResizeDisk_Run(t *testing.T) { func TestStepResizeDisk_Skips(t *testing.T) {
state := testState(t) testConfigs := []*Config{
driver := state.Get("driver").(*DriverMock) &Config{
DiskImage: false,
SkipResizeDisk: false,
},
&Config{
DiskImage: false,
SkipResizeDisk: true,
},
}
for _, config := range testConfigs {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
config := &Config{ state.Put("config", config)
DiskImage: true, step := new(stepResizeDisk)
SkipResizeDisk: false,
DiskSize: "4096M",
Format: "qcow2",
OutputDir: "/test/",
VMName: "test",
}
state.Put("config", config)
step := new(stepResizeDisk)
// Test the run // Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue { if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action) t.Fatalf("bad action: %#v", action)
} }
if _, ok := state.GetOk("error"); ok { if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error") t.Fatal("should NOT have error")
} }
if len(driver.QemuImgCalls) == 0 { if len(driver.QemuImgCalls) > 0 {
t.Fatal("should qemu-img called") t.Fatal("should NOT have called qemu-img")
} }
if len(driver.QemuImgCalls[0]) != 5 {
t.Fatal("should 5 qemu-img parameters")
} }
} }
func TestStepResizeDisk_SkipIso(t *testing.T) { func Test_buildResizeCommand(t *testing.T) {
state := testState(t) type testCase struct {
driver := state.Get("driver").(*DriverMock) Step *stepResizeDisk
config := &Config{ Expected []string
DiskImage: false, Reason string
SkipResizeDisk: false, }
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 for _, tc := range testcases {
if action := step.Run(context.Background(), state); action != multistep.ActionContinue { command := tc.Step.buildResizeCommand("source.qcow")
t.Fatalf("bad action: %#v", action)
} assert.Equal(t, command, tc.Expected,
if _, ok := state.GetOk("error"); ok { fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected))
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")
} }
} }

View File

@ -247,7 +247,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
inArgs := make(map[string][]string) inArgs := make(map[string][]string)
if len(config.QemuArgs) > 0 { 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) httpIp := state.Get("http_ip").(string)
httpPort := state.Get("http_port").(int) httpPort := state.Get("http_port").(int)

View File

@ -231,6 +231,36 @@
`{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`, `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`,
`{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}` `{{ .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 - `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 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 some platforms. For example qemu-kvm, or qemu-system-i386 may be a

View File

@ -0,0 +1,7 @@
<!-- Code generated from the comments of the QemuImgArgs struct in builder/qemu/config.go; DO NOT EDIT MANUALLY -->
- `convert` ([]string) - Convert
- `create` ([]string) - Create
- `resize` ([]string) - Resize