Merge pull request #10064 from hashicorp/refactor_step_run

builder/qemu: (tech-debt) Major refactor of step_run.
This commit is contained in:
Megan Marsh 2020-10-08 11:54:34 -07:00 committed by GitHub
commit 774a168957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 848 additions and 246 deletions

View File

@ -192,6 +192,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
artifact.state["generated_data"] = state.Get("generated_data") artifact.state["generated_data"] = state.Get("generated_data")
artifact.state["diskName"] = b.config.VMName artifact.state["diskName"] = b.config.VMName
// placed in state in step_create_disk.go
diskpaths, ok := state.Get("qemu_disk_paths").([]string) diskpaths, ok := state.Get("qemu_disk_paths").([]string)
if ok { if ok {
artifact.state["diskPaths"] = diskpaths artifact.state["diskPaths"] = diskpaths

View File

@ -16,41 +16,45 @@ import (
// stepRun runs the virtual machine // stepRun runs the virtual machine
type stepRun struct { type stepRun struct {
DiskImage bool DiskImage bool
}
type qemuArgsTemplateData struct { atLeastVersion2 bool
HTTPIP string ui packer.Ui
HTTPPort int
HTTPDir string
OutputDir string
Name string
SSHHostPort int
} }
func (s *stepRun) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { func (s *stepRun) 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) s.ui = state.Get("ui").(packer.Ui)
// Run command is different depending whether we're booting from an // Figure out version of qemu; store on step for later use
// installation CD or a pre-baked image rawVersion, err := driver.Version()
bootDrive := "once=d" if err != nil {
message := "Starting VM, booting from CD-ROM" err := fmt.Errorf("Error determining qemu version: %s", err)
if s.DiskImage { s.ui.Error(err.Error())
bootDrive = "c" return multistep.ActionHalt
message = "Starting VM, booting disk image"
} }
ui.Say(message) qemuVersion, err := version.NewVersion(rawVersion)
if err != nil {
err := fmt.Errorf("Error parsing qemu version: %s", err)
s.ui.Error(err.Error())
return multistep.ActionHalt
}
v2 := version.Must(version.NewVersion("2.0"))
command, err := getCommandArgs(bootDrive, state) s.atLeastVersion2 = qemuVersion.GreaterThanOrEqual(v2)
// Generate the qemu command
command, err := s.getCommandArgs(config, state)
if err != nil { if err != nil {
err := fmt.Errorf("Error processing QemuArgs: %s", err) err := fmt.Errorf("Error processing QemuArgs: %s", err)
ui.Error(err.Error()) s.ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
// run the qemu command
if err := driver.Qemu(command...); err != nil { if err := driver.Qemu(command...); err != nil {
err := fmt.Errorf("Error launching VM: %s", err) err := fmt.Errorf("Error launching VM: %s", err)
ui.Error(err.Error()) s.ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
@ -66,140 +70,187 @@ func (s *stepRun) Cleanup(state multistep.StateBag) {
} }
} }
func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error) { func (s *stepRun) getDefaultArgs(config *Config, state multistep.StateBag) map[string]interface{} {
config := state.Get("config").(*Config)
isoPath := state.Get("iso_path").(string)
vncIP := config.VNCBindAddress
vncPort := state.Get("vnc_port").(int)
ui := state.Get("ui").(packer.Ui)
driver := state.Get("driver").(Driver)
vmName := config.VMName
imgPath := filepath.Join(config.OutputDir, vmName)
defaultArgs := make(map[string]interface{}) defaultArgs := make(map[string]interface{})
var deviceArgs []string
var driveArgs []string // Configure "boot" arguement
var commHostPort int // Run command is different depending whether we're booting from an
// installation CD or a pre-baked image
bootDrive := "once=d"
message := "Starting VM, booting from CD-ROM"
if s.DiskImage {
bootDrive = "c"
message = "Starting VM, booting disk image"
}
s.ui.Say(message)
defaultArgs["-boot"] = bootDrive
// configure "-qmp" arguments
if config.QMPEnable {
defaultArgs["-qmp"] = fmt.Sprintf("unix:%s,server,nowait", config.QMPSocketPath)
}
// configure "-name" arguments
defaultArgs["-name"] = config.VMName
// Configure "-machine" arguments
if config.Accelerator == "none" {
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType)
s.ui.Message("WARNING: The VM will be started with no hardware acceleration.\n" +
"The installation may take considerably longer to finish.\n")
} else {
defaultArgs["-machine"] = fmt.Sprintf("type=%s,accel=%s",
config.MachineType, config.Accelerator)
}
// Configure "-netdev" arguments
defaultArgs["-netdev"] = fmt.Sprintf("bridge,id=user.0,br=%s", config.NetBridge)
if config.NetBridge == "" {
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0")
if config.CommConfig.Comm.Type != "none" {
commHostPort := state.Get("commHostPort").(int)
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", commHostPort, config.CommConfig.Comm.Port())
}
}
// Configure "-vnc" arguments
// vncPort is always set in stepConfigureVNC, so we don't need to
// defensively assert
vncPort := state.Get("vnc_port").(int)
vncIP := config.VNCBindAddress
vncPort = vncPort - config.VNCPortMin vncPort = vncPort - config.VNCPortMin
vnc := fmt.Sprintf("%s:%d", vncIP, vncPort) vnc := fmt.Sprintf("%s:%d", vncIP, vncPort)
if config.VNCUsePassword { if config.VNCUsePassword {
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort) vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort)
} }
defaultArgs["-vnc"] = vnc
if config.QMPEnable { // Track the connection for the user
defaultArgs["-qmp"] = fmt.Sprintf("unix:%s,server,nowait", config.QMPSocketPath) vncPass, _ := state.Get("vnc_password").(string)
message = getVncConnectionMessage(config.Headless, vnc, vncPass)
if message != "" {
s.ui.Message(message)
} }
defaultArgs["-name"] = vmName // Configure "-m" memory argument
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) defaultArgs["-m"] = fmt.Sprintf("%dM", config.MemorySize)
if config.NetBridge == "" { // Configure "-smp" processor hardware arguments
if config.CommConfig.Comm.Type != "none" { if config.CpuCount > 1 {
commHostPort = state.Get("commHostPort").(int) defaultArgs["-smp"] = fmt.Sprintf("cpus=%d,sockets=%d", config.CpuCount, config.CpuCount)
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", commHostPort, config.CommConfig.Comm.Port()) }
} else {
defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0") // Configure "-fda" floppy disk attachment
} if floppyPathRaw, ok := state.GetOk("floppy_path"); ok {
defaultArgs["-fda"] = floppyPathRaw.(string)
} else { } else {
defaultArgs["-netdev"] = fmt.Sprintf("bridge,id=user.0,br=%s", config.NetBridge) log.Println("Qemu Builder has no floppy files, not attaching a floppy.")
} }
rawVersion, err := driver.Version() // Configure GUI display
if err != nil { if !config.Headless {
return nil, err if s.atLeastVersion2 {
} // FIXME: "none" is a valid display option in qemu but we have
qemuVersion, err := version.NewVersion(rawVersion) // departed from the qemu usage here to instaed mean "let qemu
if err != nil { // set a reasonable default". We need to deprecate this behavior
return nil, err // and let users just set "UseDefaultDisplay" if they want to let
} // qemu do its thing.
v2 := version.Must(version.NewVersion("2.0")) if len(config.Display) > 0 && config.Display != "none" {
defaultArgs["-display"] = config.Display
if qemuVersion.GreaterThanOrEqual(v2) {
if config.DiskInterface == "virtio-scsi" {
if config.DiskImage {
deviceArgs = append(deviceArgs, "virtio-scsi-pci,id=scsi0", "scsi-hd,bus=scsi0.0,drive=drive0")
driveArgumentString := fmt.Sprintf("if=none,file=%s,id=drive0,cache=%s,discard=%s,format=%s", imgPath, config.DiskCache, config.DiskDiscard, config.Format)
if config.DetectZeroes != "off" {
driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes)
}
driveArgs = append(driveArgs, driveArgumentString)
} else {
deviceArgs = append(deviceArgs, "virtio-scsi-pci,id=scsi0")
diskFullPaths := state.Get("qemu_disk_paths").([]string)
for i, diskFullPath := range diskFullPaths {
deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi0.0,drive=drive%d", i))
driveArgumentString := fmt.Sprintf("if=none,file=%s,id=drive%d,cache=%s,discard=%s,format=%s", diskFullPath, i, config.DiskCache, config.DiskDiscard, config.Format)
if config.DetectZeroes != "off" {
driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes)
}
driveArgs = append(driveArgs, driveArgumentString)
}
}
} else {
if config.DiskImage {
driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format)
if config.DetectZeroes != "off" {
driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes)
}
driveArgs = append(driveArgs, driveArgumentString)
} else {
diskFullPaths := state.Get("qemu_disk_paths").([]string)
for _, diskFullPath := range diskFullPaths {
driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", diskFullPath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format)
if config.DetectZeroes != "off" {
driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes)
}
driveArgs = append(driveArgs, driveArgumentString)
}
}
}
} else {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.Format))
}
deviceArgs = append(deviceArgs, fmt.Sprintf("%s,netdev=user.0", config.NetDevice))
if config.Headless == true {
vncPortRaw, vncPortOk := state.GetOk("vnc_port")
vncPass := state.Get("vnc_password")
if vncPortOk && vncPass != nil && len(vncPass.(string)) > 0 {
vncPort := vncPortRaw.(int)
ui.Message(fmt.Sprintf(
"The VM will be run headless, without a GUI. If you want to\n"+
"view the screen of the VM, connect via VNC to vnc://%s:%d\n"+
"with the password: %s", vncIP, vncPort, vncPass))
} else if vncPortOk {
vncPort := vncPortRaw.(int)
ui.Message(fmt.Sprintf(
"The VM will be run headless, without a GUI. If you want to\n"+
"view the screen of the VM, connect via VNC without a password to\n"+
"vnc://%s:%d", vncIP, vncPort))
} else {
ui.Message("The VM will be run headless, without a GUI, as configured.\n" +
"If the run isn't succeeding as you expect, please enable the GUI\n" +
"to inspect the progress of the build.")
}
} else {
if qemuVersion.GreaterThanOrEqual(v2) {
if len(config.Display) > 0 {
if config.Display != "none" {
defaultArgs["-display"] = config.Display
}
} else if !config.UseDefaultDisplay { } else if !config.UseDefaultDisplay {
defaultArgs["-display"] = "gtk" defaultArgs["-display"] = "gtk"
} }
} else { } else {
ui.Message("WARNING: The version of qemu on your host doesn't support display mode.\n" + s.ui.Message("WARNING: The version of qemu on your host doesn't support display mode.\n" +
"The display parameter will be ignored.") "The display parameter will be ignored.")
} }
} }
deviceArgs, driveArgs := s.getDeviceAndDriveArgs(config, state)
defaultArgs["-device"] = deviceArgs
defaultArgs["-drive"] = driveArgs
return defaultArgs
}
func getVncConnectionMessage(headless bool, vnc string, vncPass string) string {
// Configure GUI display
if headless {
if vnc == "" {
return "The VM will be run headless, without a GUI, as configured.\n" +
"If the run isn't succeeding as you expect, please enable the GUI\n" +
"to inspect the progress of the build."
}
if vncPass != "" {
return fmt.Sprintf(
"The VM will be run headless, without a GUI. If you want to\n"+
"view the screen of the VM, connect via VNC to vnc://%s\n"+
"with the password: %s", vnc, vncPass)
}
return fmt.Sprintf(
"The VM will be run headless, without a GUI. If you want to\n"+
"view the screen of the VM, connect via VNC without a password to\n"+
"vnc://%s", vnc)
}
return ""
}
func (s *stepRun) getDeviceAndDriveArgs(config *Config, state multistep.StateBag) ([]string, []string) {
var deviceArgs []string
var driveArgs []string
vmName := config.VMName
imgPath := filepath.Join(config.OutputDir, vmName)
// Configure virtual hard drives
if s.atLeastVersion2 {
// We have different things to attach based on whether we are booting
// from an iso or a boot image.
drivesToAttach := []string{}
if config.DiskImage {
drivesToAttach = append(drivesToAttach, imgPath)
}
diskFullPaths := state.Get("qemu_disk_paths").([]string)
drivesToAttach = append(drivesToAttach, diskFullPaths...)
for i, drivePath := range drivesToAttach {
driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", drivePath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format)
if config.DiskInterface == "virtio-scsi" {
// TODO: Megan: Remove this conditional. This, and the code
// under the TODO below, reproduce the old behavior. While it
// may be broken, the goal of this commit is to refactor in a way
// that creates a result that is testably the same as the old
// code. A pr will follow fixing this broken behavior.
if i == 0 {
deviceArgs = append(deviceArgs, fmt.Sprintf("virtio-scsi-pci,id=scsi%d", i))
}
// TODO: Megan: When you remove above conditional,
// set deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi%d.0,drive=drive%d", i, i))
deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi0.0,drive=drive%d", i))
driveArgumentString = fmt.Sprintf("if=none,file=%s,id=drive%d,cache=%s,discard=%s,format=%s", drivePath, i, config.DiskCache, config.DiskDiscard, config.Format)
}
if config.DetectZeroes != "off" {
driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes)
}
driveArgs = append(driveArgs, driveArgumentString)
}
} else {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.Format))
}
deviceArgs = append(deviceArgs, fmt.Sprintf("%s,netdev=user.0", config.NetDevice))
// Configure virtual CDs
cdPaths := []string{} cdPaths := []string{}
// Add the installation CD to the run command // Add the installation CD to the run command
if !config.DiskImage { if !config.DiskImage {
isoPath := state.Get("iso_path").(string)
cdPaths = append(cdPaths, isoPath) cdPaths = append(cdPaths, isoPath)
} }
// Add our custom CD created from cd_files, if it exists // Add our custom CD created from cd_files, if it exists
@ -220,64 +271,48 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
} }
} }
defaultArgs["-device"] = deviceArgs return deviceArgs, driveArgs
defaultArgs["-drive"] = driveArgs }
defaultArgs["-boot"] = bootDrive func (s *stepRun) applyUserOverrides(defaultArgs map[string]interface{}, config *Config, state multistep.StateBag) ([]string, error) {
defaultArgs["-m"] = fmt.Sprintf("%dM", config.MemorySize) // Done setting up defaults; time to process user args and defaults together
if config.CpuCount > 1 { // and generate output args
defaultArgs["-smp"] = fmt.Sprintf("cpus=%d,sockets=%d", config.CpuCount, config.CpuCount)
}
defaultArgs["-vnc"] = vnc
// Append the accelerator to the machine type if it is specified
if config.Accelerator != "none" {
defaultArgs["-machine"] = fmt.Sprintf("%s,accel=%s", defaultArgs["-machine"], config.Accelerator)
} else {
ui.Message("WARNING: The VM will be started with no hardware acceleration.\n" +
"The installation may take considerably longer to finish.\n")
}
// Determine if we have a floppy disk to attach
if floppyPathRaw, ok := state.GetOk("floppy_path"); ok {
defaultArgs["-fda"] = floppyPathRaw.(string)
} else {
log.Println("Qemu Builder has no floppy files, not attaching a floppy.")
}
inArgs := make(map[string][]string) inArgs := make(map[string][]string)
if len(config.QemuArgs) > 0 { if len(config.QemuArgs) > 0 {
ui.Say("Overriding default Qemu arguments with QemuArgs...") s.ui.Say("Overriding default Qemu arguments with qemuargs template option...")
commHostPort := state.Get("commHostPort").(int)
httpIp := state.Get("http_ip").(string) httpIp := state.Get("http_ip").(string)
httpPort := state.Get("http_port").(int) httpPort := state.Get("http_port").(int)
ictx := config.ctx
if config.CommConfig.Comm.Type != "none" { type qemuArgsTemplateData struct {
ictx.Data = qemuArgsTemplateData{ HTTPIP string
httpIp, HTTPPort int
httpPort, HTTPDir string
config.HTTPDir, OutputDir string
config.OutputDir, Name string
config.VMName, SSHHostPort int
commHostPort,
}
} else {
ictx.Data = qemuArgsTemplateData{
HTTPIP: httpIp,
HTTPPort: httpPort,
HTTPDir: config.HTTPDir,
OutputDir: config.OutputDir,
Name: config.VMName,
}
} }
ictx := config.ctx
ictx.Data = qemuArgsTemplateData{
HTTPIP: httpIp,
HTTPPort: httpPort,
HTTPDir: config.HTTPDir,
OutputDir: config.OutputDir,
Name: config.VMName,
SSHHostPort: commHostPort,
}
// Interpolate each string in qemuargs
newQemuArgs, err := processArgs(config.QemuArgs, &ictx) newQemuArgs, err := processArgs(config.QemuArgs, &ictx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// because qemu supports multiple appearances of the same // Qemu supports multiple appearances of the same switch. This means
// switch, just different values, each key in the args hash // each key in the args hash will have an array of string values
// will have an array of string values
for _, qemuArgs := range newQemuArgs { for _, qemuArgs := range newQemuArgs {
key := qemuArgs[0] key := qemuArgs[0]
val := strings.Join(qemuArgs[1:], "") val := strings.Join(qemuArgs[1:], "")
@ -326,6 +361,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
return outArgs, nil return outArgs, nil
} }
func (s *stepRun) getCommandArgs(config *Config, state multistep.StateBag) ([]string, error) {
defaultArgs := s.getDefaultArgs(config, state)
return s.applyUserOverrides(defaultArgs, config, state)
}
func processArgs(args [][]string, ctx *interpolate.Context) ([][]string, error) { func processArgs(args [][]string, ctx *interpolate.Context) ([][]string, error) {
var err error var err error

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"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" "github.com/stretchr/testify/assert"
@ -11,8 +13,6 @@ import (
func runTestState(t *testing.T, config *Config) multistep.StateBag { func runTestState(t *testing.T, config *Config) multistep.StateBag {
state := new(multistep.BasicStateBag) state := new(multistep.BasicStateBag)
state.Put("ui", packer.TestUi(t))
state.Put("config", config) state.Put("config", config)
d := new(DriverMock) d := new(DriverMock)
@ -31,86 +31,396 @@ func runTestState(t *testing.T, config *Config) multistep.StateBag {
return state return state
} }
func Test_getCommandArgs(t *testing.T) { func Test_UserOverrides(t *testing.T) {
state := runTestState(t, &Config{}) type testCase struct {
Config *Config
args, err := getCommandArgs("", state) Expected []string
if err != nil { Reason string
t.Fatalf("should not have an error getting args. Error: %s", err) }
testcases := []testCase{
{
&Config{
HTTPConfig: common.HTTPConfig{
HTTPDir: "http/directory",
},
OutputDir: "output/directory",
VMName: "myvm",
QemuArgs: [][]string{
{"-randomflag1", "{{.HTTPIP}}-{{.HTTPPort}}-{{.HTTPDir}}"},
{"-randomflag2", "{{.OutputDir}}-{{.Name}}"},
},
},
[]string{
"-display", "gtk",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
"-randomflag1", "127.0.0.1-1234-http/directory",
"-randomflag2", "output/directory-myvm",
"-device", ",netdev=user.0",
},
"Test that interpolation overrides work.",
},
{
&Config{
VMName: "myvm",
QemuArgs: [][]string{{"-display", "partydisplay"}},
},
[]string{
"-display", "partydisplay",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
"-device", ",netdev=user.0",
},
"User input overrides default, rest is populated as normal",
},
{
&Config{
VMName: "myvm",
NetDevice: "mynetdevice",
QemuArgs: [][]string{{"-device", "somerandomdevice"}},
},
[]string{
"-display", "gtk",
"-device", "somerandomdevice",
"-device", "mynetdevice,netdev=user.0",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
},
"Net device gets added",
},
} }
expected := []string{ for _, tc := range testcases {
"-display", "gtk", state := runTestState(t, tc.Config)
"-m", "0M",
"-boot", "", step := &stepRun{
"-fda", "fake_floppy_path", atLeastVersion2: true,
"-name", "", ui: packer.TestUi(t),
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0", }
"-vnc", ":5905", args, err := step.getCommandArgs(tc.Config, state)
"-machine", "type=,accel=", if err != nil {
"-device", ",netdev=user.0", t.Fatalf("should not have an error getting args. Error: %s", err)
"-drive", "file=/path/to/test.iso,index=0,media=cdrom", }
expected := append([]string{
"-m", "0M",
"-boot", "once=d",
"-fda", "fake_floppy_path",
"-name", "myvm",
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0",
"-vnc", ":5905",
"-machine", "type=,accel="},
tc.Expected...)
assert.ElementsMatch(t, args, expected,
fmt.Sprintf("%s, \nRecieved: %#v", tc.Reason, args))
} }
assert.ElementsMatch(t, args, expected, "unexpected generated args")
} }
func Test_CDFilesPath(t *testing.T) { func Test_DriveAndDeviceArgs(t *testing.T) {
// cd_path is set and DiskImage is false type testCase struct {
state := runTestState(t, &Config{}) Config *Config
state.Put("cd_path", "fake_cd_path.iso") ExtraState map[string]interface{}
Step *stepRun
args, err := getCommandArgs("", state) Expected []string
if err != nil { Reason string
t.Fatalf("should not have an error getting args. Error: %s", err)
} }
expected := []string{ testcases := []testCase{
"-display", "gtk", {
"-m", "0M", &Config{},
"-boot", "", map[string]interface{}{},
"-fda", "fake_floppy_path", &stepRun{
"-name", "", atLeastVersion2: true,
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0", ui: packer.TestUi(t),
"-vnc", ":5905", },
"-machine", "type=,accel=", []string{
"-device", ",netdev=user.0", "-display", "gtk",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom", "-boot", "once=d",
"-drive", "file=fake_cd_path.iso,index=1,media=cdrom", "-drive", "file=/path/to/test.iso,index=0,media=cdrom",
},
"Boot value should default to once=d when diskImage isn't set",
},
{
&Config{
DiskImage: true,
DiskInterface: "virtio-scsi",
OutputDir: "path_to_output",
DiskCache: "writeback",
Format: "qcow2",
DetectZeroes: "off",
},
map[string]interface{}{
"cd_path": "fake_cd_path.iso",
},
&stepRun{
DiskImage: true,
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "c",
"-device", "virtio-scsi-pci,id=scsi0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive0",
"-drive", "if=none,file=path_to_output,id=drive0,cache=writeback,discard=,format=qcow2",
"-drive", "file=fake_cd_path.iso,index=0,media=cdrom",
},
"virtio-scsi interface, DiskImage true, extra cdrom, detectZeroes off",
},
{
&Config{
DiskImage: true,
DiskInterface: "virtio-scsi",
OutputDir: "path_to_output",
DiskCache: "writeback",
Format: "qcow2",
DetectZeroes: "on",
},
map[string]interface{}{
"cd_path": "fake_cd_path.iso",
},
&stepRun{
DiskImage: true,
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "c",
"-device", "virtio-scsi-pci,id=scsi0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive0",
"-drive", "if=none,file=path_to_output,id=drive0,cache=writeback,discard=,format=qcow2,detect-zeroes=on",
"-drive", "file=fake_cd_path.iso,index=0,media=cdrom",
},
"virtio-scsi interface, DiskImage true, extra cdrom, detectZeroes on",
},
{
&Config{
DiskInterface: "virtio-scsi",
OutputDir: "path_to_output",
DiskCache: "writeback",
Format: "qcow2",
DetectZeroes: "off",
},
map[string]interface{}{
"cd_path": "fake_cd_path.iso",
// when disk image is false, we will always have at least one
// disk path: the one we create to be the main disk.
"qemu_disk_paths": []string{"qemupath1", "qemupath2"},
},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "once=d",
"-device", "virtio-scsi-pci,id=scsi0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive1",
"-drive", "if=none,file=qemupath1,id=drive0,cache=writeback,discard=,format=qcow2",
"-drive", "if=none,file=qemupath2,id=drive1,cache=writeback,discard=,format=qcow2",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
"-drive", "file=fake_cd_path.iso,index=1,media=cdrom",
},
"virtio-scsi interface, bootable iso, cdrom",
},
{
&Config{
DiskInterface: "virtio-scsi",
OutputDir: "path_to_output",
DiskCache: "writeback",
Format: "qcow2",
DetectZeroes: "on",
},
map[string]interface{}{
"cd_path": "fake_cd_path.iso",
// when disk image is false, we will always have at least one
// disk path: the one we create to be the main disk.
"qemu_disk_paths": []string{"qemupath1", "qemupath2"},
},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "once=d",
"-device", "virtio-scsi-pci,id=scsi0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive1",
"-drive", "if=none,file=qemupath1,id=drive0,cache=writeback,discard=,format=qcow2,detect-zeroes=on",
"-drive", "if=none,file=qemupath2,id=drive1,cache=writeback,discard=,format=qcow2,detect-zeroes=on",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
"-drive", "file=fake_cd_path.iso,index=1,media=cdrom",
},
"virtio-scsi interface, DiskImage false, extra cdrom, detect zeroes on",
},
{
&Config{
DiskInterface: "virtio-scsi",
OutputDir: "path_to_output",
DiskCache: "writeback",
Format: "qcow2",
},
map[string]interface{}{
// when disk image is false, we will always have at least one
// disk path: the one we create to be the main disk.
"qemu_disk_paths": []string{"output/dir/path/mydisk.qcow2"},
},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "once=d",
"-device", "virtio-scsi-pci,id=scsi0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive0",
"-drive", "if=none,file=output/dir/path/mydisk.qcow2,id=drive0,cache=writeback,discard=,format=qcow2,detect-zeroes=",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
},
"virtio-scsi interface, DiskImage false, no extra disks or cds",
},
{
&Config{},
map[string]interface{}{
"cd_path": "fake_cd_path.iso",
"qemu_disk_paths": []string{"output/dir/path/mydisk.qcow2"},
},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "once=d",
"-drive", "file=output/dir/path/mydisk.qcow2,if=,cache=,discard=,format=,detect-zeroes=",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
"-drive", "file=fake_cd_path.iso,index=1,media=cdrom",
},
"cd_path is set and DiskImage is false",
},
{
&Config{},
map[string]interface{}{
// when disk image is false, we will always have at least one
// disk path: the one we create to be the main disk.
"qemu_disk_paths": []string{"output/dir/path/mydisk.qcow2"},
},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "once=d",
"-drive", "file=output/dir/path/mydisk.qcow2,if=,cache=,discard=,format=,detect-zeroes=",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
},
"empty config",
},
{
&Config{
OutputDir: "path_to_output",
DiskInterface: "virtio",
DiskCache: "writeback",
Format: "qcow2",
},
map[string]interface{}{
// when disk image is false, we will always have at least one
// disk path: the one we create to be the main disk.
"qemu_disk_paths": []string{"output/dir/path/mydisk.qcow2"},
},
&stepRun{
atLeastVersion2: false,
ui: packer.TestUi(t),
},
[]string{
"-boot", "once=d",
"-drive", "file=path_to_output,if=virtio,cache=writeback,format=qcow2",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
},
"version less than 2",
},
{
&Config{
OutputDir: "path_to_output",
DiskInterface: "virtio",
DiskCache: "writeback",
Format: "qcow2",
},
map[string]interface{}{
"cd_path": "fake_cd_path.iso",
"qemu_disk_paths": []string{"qemupath1", "qemupath2"},
},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "once=d",
"-drive", "file=qemupath1,if=virtio,cache=writeback,discard=,format=qcow2,detect-zeroes=",
"-drive", "file=qemupath2,if=virtio,cache=writeback,discard=,format=qcow2,detect-zeroes=",
"-drive", "file=fake_cd_path.iso,index=1,media=cdrom",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
},
"virtio interface with extra disks",
},
{
&Config{
DiskImage: true,
OutputDir: "path_to_output",
DiskInterface: "virtio",
DiskCache: "writeback",
Format: "qcow2",
},
map[string]interface{}{
"cd_path": "fake_cd_path.iso",
},
&stepRun{
DiskImage: true,
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{
"-display", "gtk",
"-boot", "c",
"-drive", "file=path_to_output,if=virtio,cache=writeback,discard=,format=qcow2,detect-zeroes=",
"-drive", "file=fake_cd_path.iso,index=0,media=cdrom",
},
"virtio interface with disk image",
},
} }
for _, tc := range testcases {
state := runTestState(t, &Config{})
for k, v := range tc.ExtraState {
state.Put(k, v)
}
assert.ElementsMatch(t, args, expected, fmt.Sprintf("unexpected generated args: %#v", args)) args, err := tc.Step.getCommandArgs(tc.Config, state)
if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err)
}
// cd_path is set and DiskImage is true expected := append([]string{
config := &Config{ "-m", "0M",
DiskImage: true, "-fda", "fake_floppy_path",
DiskInterface: "virtio-scsi", "-name", "",
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0",
"-vnc", ":5905",
"-machine", "type=,accel=",
"-device", ",netdev=user.0"},
tc.Expected...)
assert.ElementsMatch(t, args, expected,
fmt.Sprintf("%s, \nRecieved: %#v", tc.Reason, args))
} }
state = runTestState(t, config)
state.Put("cd_path", "fake_cd_path.iso")
args, err = getCommandArgs("c", state)
if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err)
}
expected = []string{
"-display", "gtk",
"-m", "0M",
"-boot", "c",
"-fda", "fake_floppy_path",
"-name", "",
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0",
"-vnc", ":5905",
"-machine", "type=,accel=",
"-device", ",netdev=user.0",
"-device", "virtio-scsi-pci,id=scsi0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive0",
"-drive", "if=none,file=,id=drive0,cache=,discard=,format=,detect-zeroes=",
"-drive", "file=fake_cd_path.iso,index=0,media=cdrom",
}
assert.ElementsMatch(t, args, expected, fmt.Sprintf("unexpected generated args: %#v", args))
} }
func Test_OptionalConfigOptionsGetSet(t *testing.T) { func Test_OptionalConfigOptionsGetSet(t *testing.T) {
@ -124,8 +434,11 @@ func Test_OptionalConfigOptionsGetSet(t *testing.T) {
} }
state := runTestState(t, c) state := runTestState(t, c)
step := &stepRun{
args, err := getCommandArgs("once=d", state) atLeastVersion2: true,
ui: packer.TestUi(t),
}
args, err := step.getCommandArgs(c, state)
if err != nil { if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err) t.Fatalf("should not have an error getting args. Error: %s", err)
} }
@ -144,5 +457,251 @@ func Test_OptionalConfigOptionsGetSet(t *testing.T) {
"-qmp", "unix:qmp_path,server,nowait", "-qmp", "unix:qmp_path,server,nowait",
} }
assert.ElementsMatch(t, args, expected, "password flag should be set, and d drive should be set.") assert.ElementsMatch(t, args, expected, "password flag should be set, and d drive should be set: %s", args)
}
// Tests for presence of Packer-generated arguments. Doesn't test that
// arguments which shouldn't be there are absent.
func Test_Defaults(t *testing.T) {
type testCase struct {
Config *Config
ExtraState map[string]interface{}
Step *stepRun
Expected []string
Reason string
}
testcases := []testCase{
{
&Config{},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-boot", "once=d"},
"Boot value should default to once=d",
},
{
&Config{},
map[string]interface{}{},
&stepRun{
DiskImage: true,
ui: packer.TestUi(t),
},
[]string{"-boot", "c"},
"Boot value should be set to c when DiskImage is set on step",
},
{
&Config{
QMPEnable: true,
QMPSocketPath: "/path/to/socket",
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-qmp", "unix:/path/to/socket,server,nowait"},
"Args should contain -qmp when qmp_enable is set",
},
{
&Config{
QMPEnable: true,
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-qmp", "unix:,server,nowait"},
"Args contain -qmp even when socket path isn't set, if qmp enabled",
},
{
&Config{
VMName: "partyname",
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-name", "partyname"},
"Name is set from config",
},
{
&Config{},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-name", ""},
"Name is set from config, even when name is blank (which won't " +
"happen for real thanks to defaulting in build prepare)",
},
{
&Config{
Accelerator: "none",
MachineType: "fancymachine",
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-machine", "type=fancymachine"},
"Don't add accelerator tag when no accelerator is set.",
},
{
&Config{
Accelerator: "kvm",
MachineType: "fancymachine",
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-machine", "type=fancymachine,accel=kvm"},
"Add accelerator tag when accelerator is set.",
},
{
&Config{
NetBridge: "fakebridge",
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-netdev", "bridge,id=user.0,br=fakebridge"},
"Add netbridge tag when netbridge is set.",
},
{
&Config{
CommConfig: CommConfig{
Comm: communicator.Config{
Type: "none",
},
},
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-netdev", "user,id=user.0"},
"No host forwarding when no net bridge and no communicator",
},
{
&Config{
CommConfig: CommConfig{
Comm: communicator.Config{
Type: "ssh",
SSH: communicator.SSH{
SSHPort: 4567,
},
},
},
},
map[string]interface{}{
"commHostPort": 1111,
},
&stepRun{ui: packer.TestUi(t)},
[]string{"-netdev", "user,id=user.0,hostfwd=tcp::1111-:4567"},
"Host forwarding when a communicator is configured",
},
{
&Config{
VNCBindAddress: "1.1.1.1",
},
map[string]interface{}{
"vnc_port": 5959,
},
&stepRun{ui: packer.TestUi(t)},
[]string{"-vnc", "1.1.1.1:5959"},
"no VNC password should be set",
},
{
&Config{
VNCBindAddress: "1.1.1.1",
VNCUsePassword: true,
},
map[string]interface{}{
"vnc_port": 5959,
},
&stepRun{ui: packer.TestUi(t)},
[]string{"-vnc", "1.1.1.1:5959,password"},
"VNC password should be set",
},
{
&Config{
MemorySize: 2345,
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-m", "2345M"},
"Memory is set, with unit M",
},
{
&Config{
CpuCount: 2,
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-smp", "cpus=2,sockets=2"},
"both cpus and sockets are set to config's CpuCount",
},
{
&Config{
CpuCount: 2,
},
map[string]interface{}{},
&stepRun{ui: packer.TestUi(t)},
[]string{"-smp", "cpus=2,sockets=2"},
"both cpus and sockets are set to config's CpuCount",
},
{
&Config{
CpuCount: 2,
},
map[string]interface{}{
"floppy_path": "/path/to/floppy",
},
&stepRun{ui: packer.TestUi(t)},
[]string{"-fda", "/path/to/floppy"},
"floppy path should be set under fda flag, when it exists",
},
{
&Config{
Headless: false,
Display: "fakedisplay",
UseDefaultDisplay: false,
},
map[string]interface{}{},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{"-display", "fakedisplay"},
"Display option should value config display",
},
{
&Config{
Headless: false,
},
map[string]interface{}{},
&stepRun{
atLeastVersion2: true,
ui: packer.TestUi(t),
},
[]string{"-display", "gtk"},
"Display option should default to gtk",
},
}
for _, tc := range testcases {
state := runTestState(t, &Config{})
for k, v := range tc.ExtraState {
state.Put(k, v)
}
args, err := tc.Step.getCommandArgs(tc.Config, state)
if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err)
}
if !matchArgument(args, tc.Expected) {
t.Fatalf("Couldn't find %#v in result. Got: %#v, Reason: %s",
tc.Expected, args, tc.Reason)
}
}
}
// This test makes sure that arguments don't end up in the final boot command
// if they aren't configured in the config.
// func TestDefaultsAbsentValues(t *testing.T) {}
func matchArgument(actual []string, expected []string) bool {
key := expected[0]
for i, k := range actual {
if key == k {
if expected[1] == actual[i+1] {
return true
}
}
}
return false
} }