diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index 83169f38e..8f0d1151e 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -58,6 +58,7 @@ type config struct { BootCommand []string `mapstructure:"boot_command"` DiskInterface string `mapstructure:"disk_interface"` DiskSize uint `mapstructure:"disk_size"` + FloppyFiles []string `mapstructure:"floppy_files"` Format string `mapstructure:"format"` Headless bool `mapstructure:"headless"` HTTPDir string `mapstructure:"http_directory"` @@ -79,6 +80,7 @@ type config struct { VNCPortMin uint `mapstructure:"vnc_port_min"` VNCPortMax uint `mapstructure:"vnc_port_max"` VMName string `mapstructure:"vm_name"` + RunOnce bool `mapstructure:"run_once"` RawBootWait string `mapstructure:"boot_wait"` RawSingleISOUrl string `mapstructure:"iso_url"` @@ -150,8 +152,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { b.config.VNCPortMax = 6000 } - if b.config.QemuArgs == nil { - b.config.QemuArgs = make([][]string, 0) + for i, args := range b.config.QemuArgs { + for j, arg := range args { + if err := b.config.tpl.Validate(arg); err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Error processing qemu-system_x86-64[%d][%d]: %s", i, j, err)) + } + } } if b.config.VMName == "" { @@ -162,6 +169,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { b.config.Format = "qcow2" } + if b.config.FloppyFiles == nil { + b.config.FloppyFiles = make([]string, 0) + } + if b.config.NetDevice == "" { b.config.NetDevice = "virtio-net" } @@ -215,6 +226,16 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } } + for i, file := range b.config.FloppyFiles { + var err error + b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil) + if err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Error processing floppy_files[%d]: %s", + i, err)) + } + } + if !(b.config.Format == "qcow2" || b.config.Format == "raw") { errs = packer.MultiErrorAppend( errs, errors.New("invalid format, only 'qcow2' or 'img' are allowed")) @@ -336,13 +357,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) } - for i, args := range b.config.QemuArgs { - for j, arg := range args { - if err := b.config.tpl.Validate(arg); err != nil { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("Error processing qemu-system_x86-64[%d][%d]: %s", i, j, err)) - } - } + if b.config.QemuArgs == nil { + b.config.QemuArgs = make([][]string, 0) } if errs != nil && len(errs.Errors) > 0 { @@ -368,6 +384,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Url: b.config.ISOUrls, }, new(stepPrepareOutputDir), + &common.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, new(stepCreateDisk), new(stepHTTPServer), new(stepForwardSSH), @@ -376,15 +395,23 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe BootDrive: "d", Message: "Starting VM, booting from CD-ROM", }, - &stepBootWait{}, - &stepTypeBootCommand{}, - &stepWaitForShutdown{ - Message: "Waiting for initial VM boot to shut down", - }, - &stepRun{ - BootDrive: "c", - Message: "Starting VM, booting from hard disk", - }, + } + + if !b.config.RunOnce { + steps = append(steps, + &stepBootWait{}, + &stepTypeBootCommand{}, + &stepWaitForShutdown{ + Message: "Waiting for initial VM boot to shut down", + }, + &stepRun{ + BootDrive: "c", + Message: "Starting VM, booting from hard disk", + }, + ) + } + + steps = append(steps, &common.StepConnectSSH{ SSHAddress: sshAddress, SSHConfig: sshConfig, @@ -392,7 +419,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe }, new(common.StepProvision), new(stepShutdown), - } + ) // Setup the state bag state := new(multistep.BasicStateBag) diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go index 480d008fc..858c8424c 100644 --- a/builder/qemu/step_run.go +++ b/builder/qemu/step_run.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "log" "path/filepath" "strings" ) @@ -14,13 +15,27 @@ type stepRun struct { Message string } +type qemuArgsTemplateData struct { + HTTPIP string + HTTPPort uint + HTTPDir string + OutputDir string + Name string +} + func (s *stepRun) Run(state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) ui.Say(s.Message) - command := getCommandArgs(s.BootDrive, state) + command, err := getCommandArgs(s.BootDrive, state) + if err != nil { + err := fmt.Errorf("Error processing QemuArggs: %s", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + if err := driver.Qemu(command...); err != nil { err := fmt.Errorf("Error launching VM: %s", err) ui.Error(err.Error()) @@ -39,7 +54,7 @@ func (s *stepRun) Cleanup(state multistep.StateBag) { } } -func getCommandArgs(bootDrive string, state multistep.StateBag) []string { +func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error) { config := state.Get("config").(*config) isoPath := state.Get("iso_path").(string) vncPort := state.Get("vnc_port").(uint) @@ -72,14 +87,34 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) []string { defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort) defaultArgs["-vnc"] = vnc + // 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) if len(config.QemuArgs) > 0 { ui.Say("Overriding defaults Qemu arguments with QemuArgs...") - // becuase qemu supports multiple appearances of the same + httpPort := state.Get("http_port").(uint) + tplData := qemuArgsTemplateData{ + "10.0.2.2", + httpPort, + config.HTTPDir, + config.OutputDir, + config.VMName, + } + newQemuArgs, err := processArgs(config.QemuArgs, config.tpl, &tplData) + if err != nil { + return nil, err + } + + // because qemu supports multiple appearances of the same // switch, just different values, each key in the args hash // will have an array of string values - for _, qemuArgs := range config.QemuArgs { + for _, qemuArgs := range newQemuArgs { key := qemuArgs[0] val := strings.Join(qemuArgs[1:], "") if _, ok := inArgs[key]; !ok { @@ -112,5 +147,27 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) []string { } } - return outArgs + return outArgs, nil +} + +func processArgs(args [][]string, tpl *packer.ConfigTemplate, tplData *qemuArgsTemplateData) ([][]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 = tpl.Process(parm, &tplData) + if err != nil { + return nil, err + } + } + } + + return newArgs, err } diff --git a/website/source/docs/builders/qemu.html.markdown b/website/source/docs/builders/qemu.html.markdown index cfcd2601e..d314d99f2 100644 --- a/website/source/docs/builders/qemu.html.markdown +++ b/website/source/docs/builders/qemu.html.markdown @@ -118,6 +118,14 @@ Optional: * `format` (string) - Either "qcow2" or "img", this specifies the output format of the virtual machine image. This defaults to "qcow2". +* `floppy_files` (array of strings) - A list of files to place onto a floppy + disk that gets attached when Packer powers up the VM. This is most useful + for unattended Windows installs, which look for an `Autounattend.xml` file + on removable media. By default no floppy will be attached. All files + listed in this setting get placed into the root directory of the floppy + and teh floppy is attached as the first floppy device. Currently, no + support exists for sub-directories. + * `headless` (bool) - Packer defaults to building virtual machines by launching a GUI that shows the console of the machine being built. When this value is set to true, the machine will start without a console. @@ -191,6 +199,16 @@ Optional: By default this is "output-BUILDNAME" where "BUILDNAME" is the name of the build. +* `run_once` (boolean) - When set to true, run_once causes the builder to run + Qemu only once, rather than twice. Normally (default false) the builder + will run Qemu once for an initial OS install, then switch the CDROM device + and boot device for a second run. In that case, Packer does not wait for + SSH connections until the second power up of the VM. This approach is often + necessary in Linux distribution installs. However, in many Windows unattended + installs, the setup handles reboots and dealing with the CDROM as the boot + device. With care, a power-down/power-up setting (run_once is set to false) + could work if the unattended install is set to restart into audit mode. + * `shutdown_command` (string) - The command to use to gracefully shut down the machine once all the provisioning is done. By default this is an empty string, which tells Packer to just forcefully shut down the machine.