- generating defaults - overriding defaults with user args The default generation has been shuffled around some, in order to make sure that any changes to a specific arg happen in one place to make it easier to reason about those args. Related args have been moved close to one another. The deviceArgs and driveArgs were overly complex after several layers of copy/paste modifications. Careful pruning reduced the layers of logic and repeated code, to help make it easier to reason about.
391 lines
12 KiB
Go
391 lines
12 KiB
Go
package qemu
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/packer/helper/multistep"
|
|
"github.com/hashicorp/packer/packer"
|
|
"github.com/hashicorp/packer/template/interpolate"
|
|
)
|
|
|
|
// stepRun runs the virtual machine
|
|
type stepRun struct {
|
|
DiskImage bool
|
|
|
|
atLeastVersion2 bool
|
|
ui packer.Ui
|
|
}
|
|
|
|
func (s *stepRun) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
|
config := state.Get("config").(*Config)
|
|
driver := state.Get("driver").(Driver)
|
|
s.ui = state.Get("ui").(packer.Ui)
|
|
|
|
// Figure out version of qemu; store on step for later use
|
|
rawVersion, err := driver.Version()
|
|
if err != nil {
|
|
err := fmt.Errorf("Error determining qemu version: %s", err)
|
|
s.ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
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"))
|
|
|
|
s.atLeastVersion2 = qemuVersion.GreaterThanOrEqual(v2)
|
|
|
|
// Generate the qemu command
|
|
command, err := s.getCommandArgs(config, state)
|
|
if err != nil {
|
|
err := fmt.Errorf("Error processing QemuArgs: %s", err)
|
|
s.ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
// run the qemu command
|
|
if err := driver.Qemu(command...); err != nil {
|
|
err := fmt.Errorf("Error launching VM: %s", err)
|
|
s.ui.Error(err.Error())
|
|
return multistep.ActionHalt
|
|
}
|
|
|
|
return multistep.ActionContinue
|
|
}
|
|
|
|
func (s *stepRun) Cleanup(state multistep.StateBag) {
|
|
driver := state.Get("driver").(Driver)
|
|
ui := state.Get("ui").(packer.Ui)
|
|
|
|
if err := driver.Stop(); err != nil {
|
|
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
|
|
}
|
|
}
|
|
|
|
func (s *stepRun) getDefaultArgs(config *Config, state multistep.StateBag) map[string]interface{} {
|
|
|
|
defaultArgs := make(map[string]interface{})
|
|
|
|
// Configure "boot" arguement
|
|
// 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
|
|
vnc := fmt.Sprintf("%s:%d", vncIP, vncPort)
|
|
if config.VNCUsePassword {
|
|
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort)
|
|
}
|
|
defaultArgs["-vnc"] = vnc
|
|
|
|
// Track the connection for the user
|
|
vncPass, _ := state.Get("vnc_password").(string)
|
|
|
|
message = getVncConnectionMessage(config.Headless, vnc, vncPass)
|
|
if message != "" {
|
|
s.ui.Message(message)
|
|
}
|
|
|
|
// Configure "-m" memory argument
|
|
defaultArgs["-m"] = fmt.Sprintf("%dM", config.MemorySize)
|
|
|
|
// Configure "-smp" processor hardware arguments
|
|
if config.CpuCount > 1 {
|
|
defaultArgs["-smp"] = fmt.Sprintf("cpus=%d,sockets=%d", config.CpuCount, config.CpuCount)
|
|
}
|
|
|
|
// Configure "-fda" floppy disk attachment
|
|
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.")
|
|
}
|
|
|
|
// Configure GUI display
|
|
if !config.Headless {
|
|
if s.atLeastVersion2 {
|
|
// FIXME: "none" is a valid display option in qemu but we have
|
|
// departed from the qemu usage here to instaed mean "let qemu
|
|
// set a reasonable default". We need to deprecate this behavior
|
|
// and let users just set "UseDefaultDisplay" if they want to let
|
|
// qemu do its thing.
|
|
if len(config.Display) > 0 && config.Display != "none" {
|
|
defaultArgs["-display"] = config.Display
|
|
} else if !config.UseDefaultDisplay {
|
|
defaultArgs["-display"] = "gtk"
|
|
}
|
|
} else {
|
|
s.ui.Message("WARNING: The version of qemu on your host doesn't support display mode.\n" +
|
|
"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{}
|
|
// Add the installation CD to the run command
|
|
if !config.DiskImage {
|
|
isoPath := state.Get("iso_path").(string)
|
|
cdPaths = append(cdPaths, isoPath)
|
|
}
|
|
// Add our custom CD created from cd_files, if it exists
|
|
cdFilesPath, ok := state.Get("cd_path").(string)
|
|
if ok {
|
|
if cdFilesPath != "" {
|
|
cdPaths = append(cdPaths, cdFilesPath)
|
|
}
|
|
}
|
|
for i, cdPath := range cdPaths {
|
|
if config.CDROMInterface == "" {
|
|
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,index=%d,media=cdrom", cdPath, i))
|
|
} else if config.CDROMInterface == "virtio-scsi" {
|
|
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=none,index=%d,id=cdrom%d,media=cdrom", cdPath, i, i))
|
|
deviceArgs = append(deviceArgs, "virtio-scsi-device", fmt.Sprintf("scsi-cd,drive=cdrom%d", i))
|
|
} else {
|
|
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,index=%d,id=cdrom%d,media=cdrom", cdPath, config.CDROMInterface, i, i))
|
|
}
|
|
}
|
|
|
|
return deviceArgs, driveArgs
|
|
}
|
|
|
|
func (s *stepRun) applyUserOverrides(defaultArgs map[string]interface{}, config *Config, state multistep.StateBag) ([]string, error) {
|
|
// Done setting up defaults; time to process user args and defaults together
|
|
// and generate output args
|
|
|
|
inArgs := make(map[string][]string)
|
|
if len(config.QemuArgs) > 0 {
|
|
s.ui.Say("Overriding default Qemu arguments with qemuargs template option...")
|
|
|
|
commHostPort := state.Get("commHostPort").(int)
|
|
httpIp := state.Get("http_ip").(string)
|
|
httpPort := state.Get("http_port").(int)
|
|
|
|
type qemuArgsTemplateData struct {
|
|
HTTPIP string
|
|
HTTPPort int
|
|
HTTPDir string
|
|
OutputDir string
|
|
Name string
|
|
SSHHostPort int
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Qemu supports multiple appearances of the same switch. This means
|
|
// each key in the args hash will have an array of string values
|
|
for _, qemuArgs := range newQemuArgs {
|
|
key := qemuArgs[0]
|
|
val := strings.Join(qemuArgs[1:], "")
|
|
if _, ok := inArgs[key]; !ok {
|
|
inArgs[key] = make([]string, 0)
|
|
}
|
|
if len(val) > 0 {
|
|
inArgs[key] = append(inArgs[key], val)
|
|
}
|
|
}
|
|
}
|
|
|
|
// get any remaining missing default args from the default settings
|
|
for key := range defaultArgs {
|
|
if _, ok := inArgs[key]; !ok {
|
|
arg := make([]string, 1)
|
|
switch defaultArgs[key].(type) {
|
|
case string:
|
|
arg[0] = defaultArgs[key].(string)
|
|
case []string:
|
|
arg = defaultArgs[key].([]string)
|
|
}
|
|
inArgs[key] = arg
|
|
}
|
|
}
|
|
|
|
// Check if we are missing the netDevice #6804
|
|
if x, ok := inArgs["-device"]; ok {
|
|
if !strings.Contains(strings.Join(x, ""), config.NetDevice) {
|
|
inArgs["-device"] = append(inArgs["-device"], fmt.Sprintf("%s,netdev=user.0", config.NetDevice))
|
|
}
|
|
}
|
|
|
|
// Flatten to array of strings
|
|
outArgs := make([]string, 0)
|
|
for key, values := range inArgs {
|
|
if len(values) > 0 {
|
|
for idx := range values {
|
|
outArgs = append(outArgs, key, values[idx])
|
|
}
|
|
} else {
|
|
outArgs = append(outArgs, key)
|
|
}
|
|
}
|
|
|
|
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) {
|
|
var err error
|
|
|
|
if args == nil {
|
|
return make([][]string, 0), err
|
|
}
|
|
|
|
newArgs := make([][]string, len(args))
|
|
for argsIdx, rowArgs := range args {
|
|
parms := make([]string, len(rowArgs))
|
|
newArgs[argsIdx] = parms
|
|
for i, parm := range rowArgs {
|
|
parms[i], err = interpolate.Render(parm, ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return newArgs, err
|
|
}
|