From 30d004022e296f5db3484791b89c084dabfcdc63 Mon Sep 17 00:00:00 2001 From: Tom Hite Date: Mon, 2 Sep 2013 22:23:52 -0500 Subject: [PATCH 1/6] Initial checkin to GitHub -- has extensive changes to conform to the latest API model to match the 0.3.6 (Sept. 2, 2013) release. --- builder/qemu/artifact.go | 33 + builder/qemu/builder.go | 435 +++++++++++++ builder/qemu/builder_test.go | 571 ++++++++++++++++++ builder/qemu/driver.go | 252 ++++++++ builder/qemu/ssh.go | 59 ++ builder/qemu/step_configure_vnc.go | 53 ++ builder/qemu/step_copy_floppy.go | 84 +++ builder/qemu/step_create_disk.go | 40 ++ builder/qemu/step_forward_ssh.go | 44 ++ builder/qemu/step_http_server.go | 75 +++ builder/qemu/step_prepare_output_dir.go | 49 ++ builder/qemu/step_run.go | 134 ++++ builder/qemu/step_shutdown.go | 77 +++ builder/qemu/step_suppress_messages.go | 29 + builder/qemu/step_type_boot_command.go | 175 ++++++ config.go | 1 + plugin/builder-qemu/main.go | 10 + plugin/builder-qemu/main_test.go | 1 + .../source/docs/builders/qemu.html.markdown | 303 ++++++++++ 19 files changed, 2425 insertions(+) create mode 100644 builder/qemu/artifact.go create mode 100644 builder/qemu/builder.go create mode 100644 builder/qemu/builder_test.go create mode 100644 builder/qemu/driver.go create mode 100644 builder/qemu/ssh.go create mode 100644 builder/qemu/step_configure_vnc.go create mode 100644 builder/qemu/step_copy_floppy.go create mode 100644 builder/qemu/step_create_disk.go create mode 100644 builder/qemu/step_forward_ssh.go create mode 100644 builder/qemu/step_http_server.go create mode 100644 builder/qemu/step_prepare_output_dir.go create mode 100644 builder/qemu/step_run.go create mode 100644 builder/qemu/step_shutdown.go create mode 100644 builder/qemu/step_suppress_messages.go create mode 100644 builder/qemu/step_type_boot_command.go create mode 100644 plugin/builder-qemu/main.go create mode 100644 plugin/builder-qemu/main_test.go create mode 100644 website/source/docs/builders/qemu.html.markdown diff --git a/builder/qemu/artifact.go b/builder/qemu/artifact.go new file mode 100644 index 000000000..0fb310482 --- /dev/null +++ b/builder/qemu/artifact.go @@ -0,0 +1,33 @@ +package qemu + +import ( + "fmt" + "os" +) + +// Artifact is the result of running the VirtualBox builder, namely a set +// of files associated with the resulting machine. +type Artifact struct { + dir string + f []string +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return a.f +} + +func (*Artifact) Id() string { + return "VM" +} + +func (a *Artifact) String() string { + return fmt.Sprintf("VM files in directory: %s", a.dir) +} + +func (a *Artifact) Destroy() error { + return os.RemoveAll(a.dir) +} diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go new file mode 100644 index 000000000..c40d4b968 --- /dev/null +++ b/builder/qemu/builder.go @@ -0,0 +1,435 @@ +package qemu + +import ( + "errors" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +const BuilderId = "tdhite.qemu" + +type Builder struct { + config config + runner multistep.Runner +} + +type config struct { + common.PackerConfig `mapstructure:",squash"` + + BootCommand []string `mapstructure:"boot_command"` + DiskSize uint `mapstructure:"disk_size"` + FloppyFiles []string `mapstructure:"floppy_files"` + Format string `mapstructure:"format"` + Accelerator string `mapstructure:"accelerator"` + Headless bool `mapstructure:"headless"` + HTTPDir string `mapstructure:"http_directory"` + HTTPPortMin uint `mapstructure:"http_port_min"` + HTTPPortMax uint `mapstructure:"http_port_max"` + ISOChecksum string `mapstructure:"iso_checksum"` + ISOChecksumType string `mapstructure:"iso_checksum_type"` + ISOUrls []string `mapstructure:"iso_urls"` + OutputDir string `mapstructure:"output_directory"` + QemuArgs [][]string `mapstructure:"qemuargs"` + ShutdownCommand string `mapstructure:"shutdown_command"` + SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` + SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` + SSHPassword string `mapstructure:"ssh_password"` + SSHPort uint `mapstructure:"ssh_port"` + SSHUser string `mapstructure:"ssh_username"` + SSHKeyPath string `mapstructure:"ssh_key_path"` + VNCPortMin uint `mapstructure:"vnc_port_min"` + VNCPortMax uint `mapstructure:"vnc_port_max"` + VMName string `mapstructure:"vm_name"` + + RawBootWait string `mapstructure:"boot_wait"` + RawSingleISOUrl string `mapstructure:"iso_url"` + RawShutdownTimeout string `mapstructure:"shutdown_timeout"` + RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` + + bootWait time.Duration `` + shutdownTimeout time.Duration `` + sshWaitTimeout time.Duration `` + tpl *packer.ConfigTemplate +} + +func (b *Builder) Prepare(raws ...interface{}) error { + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return err + } + + b.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + b.config.tpl.UserVars = b.config.PackerUserVars + + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + + if b.config.DiskSize == 0 { + b.config.DiskSize = 40000 + } + + if b.config.FloppyFiles == nil { + b.config.FloppyFiles = make([]string, 0) + } + + if b.config.Accelerator == "" { + b.config.Accelerator = "kvm" + } + + if b.config.HTTPPortMin == 0 { + b.config.HTTPPortMin = 8000 + } + + if b.config.HTTPPortMax == 0 { + b.config.HTTPPortMax = 9000 + } + + if b.config.OutputDir == "" { + b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) + } + + if b.config.RawBootWait == "" { + b.config.RawBootWait = "10s" + } + + if b.config.SSHHostPortMin == 0 { + b.config.SSHHostPortMin = 2222 + } + + if b.config.SSHHostPortMax == 0 { + b.config.SSHHostPortMax = 4444 + } + + if b.config.SSHPort == 0 { + b.config.SSHPort = 22 + } + + if b.config.VNCPortMin == 0 { + b.config.VNCPortMin = 5900 + } + + if b.config.VNCPortMax == 0 { + b.config.VNCPortMax = 6000 + } + + if b.config.QemuArgs == nil { + b.config.QemuArgs = make([][]string, 0) + } + + if b.config.VMName == "" { + b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) + } + + if b.config.Format == "" { + b.config.Format = "qcow2" + } + + // Errors + templates := map[string]*string{ + "http_directory": &b.config.HTTPDir, + "iso_checksum": &b.config.ISOChecksum, + "iso_checksum_type": &b.config.ISOChecksumType, + "iso_url": &b.config.RawSingleISOUrl, + "output_directory": &b.config.OutputDir, + "shutdown_command": &b.config.ShutdownCommand, + "ssh_password": &b.config.SSHPassword, + "ssh_username": &b.config.SSHUser, + "vm_name": &b.config.VMName, + "format": &b.config.Format, + "boot_wait": &b.config.RawBootWait, + "shutdown_timeout": &b.config.RawShutdownTimeout, + "ssh_wait_timeout": &b.config.RawSSHWaitTimeout, + "accelerator": &b.config.Accelerator, + } + + for n, ptr := range templates { + var err error + *ptr, err = b.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + for i, url := range b.config.ISOUrls { + var err error + b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err)) + } + } + + for i, command := range b.config.BootCommand { + if err := b.config.tpl.Validate(command); err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Error processing boot_command[%d]: %s", i, err)) + } + } + + 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 'ovf' or 'ova' are allowed")) + } + + if !(b.config.Accelerator == "kvm" || b.config.Accelerator == "xen") { + errs = packer.MultiErrorAppend( + errs, errors.New("invalid format, only 'kvm' or 'xen' are allowed")) + } + + if b.config.HTTPPortMin > b.config.HTTPPortMax { + errs = packer.MultiErrorAppend( + errs, errors.New("http_port_min must be less than http_port_max")) + } + + if b.config.ISOChecksum == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("Due to large file sizes, an iso_checksum is required")) + } else { + b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum) + } + + if b.config.ISOChecksumType == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("The iso_checksum_type must be specified.")) + } else { + b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType) + if h := common.HashForType(b.config.ISOChecksumType); h == nil { + errs = packer.MultiErrorAppend( + errs, + fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType)) + } + } + + if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 { + errs = packer.MultiErrorAppend( + errs, errors.New("One of iso_url or iso_urls must be specified.")) + } else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 { + errs = packer.MultiErrorAppend( + errs, errors.New("Only one of iso_url or iso_urls may be specified.")) + } else if b.config.RawSingleISOUrl != "" { + b.config.ISOUrls = []string{b.config.RawSingleISOUrl} + } + + for i, url := range b.config.ISOUrls { + b.config.ISOUrls[i], err = common.DownloadableURL(url) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err)) + } + } + + if !b.config.PackerForce { + if _, err := os.Stat(b.config.OutputDir); err == nil { + errs = packer.MultiErrorAppend( + errs, + fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir)) + } + } + + b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) + } + + if b.config.RawShutdownTimeout == "" { + b.config.RawShutdownTimeout = "5m" + } + + if b.config.RawSSHWaitTimeout == "" { + b.config.RawSSHWaitTimeout = "20m" + } + + b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) + } + + if b.config.SSHKeyPath != "" { + if _, err := os.Stat(b.config.SSHKeyPath); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) + } else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) + } + } + + if b.config.SSHHostPortMin > b.config.SSHHostPortMax { + errs = packer.MultiErrorAppend( + errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) + } + + if b.config.SSHUser == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("An ssh_username must be specified.")) + } + + b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err)) + } + + if b.config.VNCPortMin > b.config.VNCPortMax { + errs = packer.MultiErrorAppend( + 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 errs != nil && len(errs.Errors) > 0 { + return errs + } + + return nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + // Create the driver that we'll use to communicate with Qemu + driver, err := b.newDriver() + if err != nil { + return nil, fmt.Errorf("Failed creating Qemu driver: %s", err) + } + + steps := []multistep.Step{ + &common.StepDownload{ + Checksum: b.config.ISOChecksum, + ChecksumType: b.config.ISOChecksumType, + Description: "ISO", + ResultKey: "iso_path", + Url: b.config.ISOUrls, + }, + new(stepPrepareOutputDir), + &common.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, + new(stepCreateDisk), + new(stepSuppressMessages), + new(stepHTTPServer), + new(stepForwardSSH), + new(stepConfigureVNC), + new(stepRun), + &common.StepConnectSSH{ + SSHAddress: sshAddress, + SSHConfig: sshConfig, + SSHWaitTimeout: b.config.sshWaitTimeout, + }, + new(common.StepProvision), + new(stepShutdown), + } + + // Setup the state bag + state := new(multistep.BasicStateBag) + state.Put("cache", cache) + state.Put("config", &b.config) + state.Put("driver", driver) + state.Put("hook", hook) + state.Put("ui", ui) + + // Run + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + + b.runner.Run(state) + + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + // If we were interrupted or cancelled, then just exit. + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return nil, errors.New("Build was cancelled.") + } + + if _, ok := state.GetOk(multistep.StateHalted); ok { + return nil, errors.New("Build was halted.") + } + + // Compile the artifact list + files := make([]string, 0, 5) + visit := func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + files = append(files, path) + } + + return err + } + + if err := filepath.Walk(b.config.OutputDir, visit); err != nil { + return nil, err + } + + artifact := &Artifact{ + dir: b.config.OutputDir, + f: files, + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} + +func (b *Builder) newDriver() (Driver, error) { + qemuPath, err := exec.LookPath("qemu-system-x86_64") + if err != nil { + return nil, err + } + + qemuImgPath, err := exec.LookPath("qemu-img") + if err != nil { + return nil, err + } + + log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath) + driver := &QemuDriver{} + driver.Initialize(qemuPath, qemuImgPath) + + if err := driver.Verify(); err != nil { + return nil, err + } + + return driver, nil +} diff --git a/builder/qemu/builder_test.go b/builder/qemu/builder_test.go new file mode 100644 index 000000000..46b09891c --- /dev/null +++ b/builder/qemu/builder_test.go @@ -0,0 +1,571 @@ +package qemu + +import ( + "github.com/mitchellh/packer/packer" + "io/ioutil" + "os" + "reflect" + "testing" +) + +var testPem = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu +hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW +LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN +AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD +2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH +uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3 +5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV +BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG +E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko +9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF +K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3 +/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+ +2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa +nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn +kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6 +hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC +v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl +b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR +v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3 +uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1 +9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR +lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc +eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa +1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG +3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4= +-----END RSA PRIVATE KEY----- +` + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "iso_checksum": "foo", + "iso_checksum_type": "md5", + "iso_url": "http://www.google.com/", + "ssh_username": "foo", + packer.BuildNameConfigKey: "foo", + } +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Error("Builder must implement builder.") + } +} + +func TestBuilderPrepare_Defaults(t *testing.T) { + var b Builder + config := testConfig() + err := b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.OutputDir != "output-foo" { + t.Errorf("bad output dir: %s", b.config.OutputDir) + } + + if b.config.SSHHostPortMin != 2222 { + t.Errorf("bad min ssh host port: %d", b.config.SSHHostPortMin) + } + + if b.config.SSHHostPortMax != 4444 { + t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax) + } + + if b.config.SSHPort != 22 { + t.Errorf("bad ssh port: %d", b.config.SSHPort) + } + + if b.config.VMName != "packer-foo" { + t.Errorf("bad vm name: %s", b.config.VMName) + } + + if b.config.Format != "qcow2" { + t.Errorf("bad format: %s", b.config.Format) + } +} + +func TestBuilderPrepare_BootWait(t *testing.T) { + var b Builder + config := testConfig() + + // Test a default boot_wait + delete(config, "boot_wait") + err := b.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if b.config.RawBootWait != "10s" { + t.Fatalf("bad value: %s", b.config.RawBootWait) + } + + // Test with a bad boot_wait + config["boot_wait"] = "this is not good" + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["boot_wait"] = "5s" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_DiskSize(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "disk_size") + err := b.Prepare(config) + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if b.config.DiskSize != 40000 { + t.Fatalf("bad size: %d", b.config.DiskSize) + } + + config["disk_size"] = 60000 + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.DiskSize != 60000 { + t.Fatalf("bad size: %s", b.config.DiskSize) + } +} + +func TestBuilderPrepare_FloppyFiles(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "floppy_files") + err := b.Prepare(config) + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if len(b.config.FloppyFiles) != 0 { + t.Fatalf("bad: %#v", b.config.FloppyFiles) + } + + config["floppy_files"] = []string{"foo", "bar"} + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + expected := []string{"foo", "bar"} + if !reflect.DeepEqual(b.config.FloppyFiles, expected) { + t.Fatalf("bad: %#v", b.config.FloppyFiles) + } +} + +func TestBuilderPrepare_HTTPPort(t *testing.T) { + var b Builder + config := testConfig() + + // Bad + config["http_port_min"] = 1000 + config["http_port_max"] = 500 + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Bad + config["http_port_min"] = -500 + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Good + config["http_port_min"] = 500 + config["http_port_max"] = 1000 + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_Format(t *testing.T) { + var b Builder + config := testConfig() + + // Bad + config["format"] = "illegal value" + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Good + config["format"] = "qcow2" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + // Good + config["format"] = "raw" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_InvalidKey(t *testing.T) { + var b Builder + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_ISOChecksum(t *testing.T) { + var b Builder + config := testConfig() + + // Test bad + config["iso_checksum"] = "" + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test good + config["iso_checksum"] = "FOo" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ISOChecksum != "foo" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksum) + } +} + +func TestBuilderPrepare_ISOChecksumType(t *testing.T) { + var b Builder + config := testConfig() + + // Test bad + config["iso_checksum_type"] = "" + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test good + config["iso_checksum_type"] = "mD5" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ISOChecksumType != "md5" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType) + } + + // Test unknown + config["iso_checksum_type"] = "fake" + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_ISOUrl(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "iso_url") + delete(config, "iso_urls") + + // Test both epty + config["iso_url"] = "" + b = Builder{} + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test iso_url set + config["iso_url"] = "http://www.packer.io" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Errorf("should not have error: %s", err) + } + + expected := []string{"http://www.packer.io"} + if !reflect.DeepEqual(b.config.ISOUrls, expected) { + t.Fatalf("bad: %#v", b.config.ISOUrls) + } + + // Test both set + config["iso_url"] = "http://www.packer.io" + config["iso_urls"] = []string{"http://www.packer.io"} + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test just iso_urls set + delete(config, "iso_url") + config["iso_urls"] = []string{ + "http://www.packer.io", + "http://www.hashicorp.com", + } + + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Errorf("should not have error: %s", err) + } + + expected = []string{ + "http://www.packer.io", + "http://www.hashicorp.com", + } + if !reflect.DeepEqual(b.config.ISOUrls, expected) { + t.Fatalf("bad: %#v", b.config.ISOUrls) + } +} + +func TestBuilderPrepare_OutputDir(t *testing.T) { + var b Builder + config := testConfig() + + // Test with existing dir + dir, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(dir) + + config["output_directory"] = dir + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["output_directory"] = "i-hope-i-dont-exist" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_ShutdownTimeout(t *testing.T) { + var b Builder + config := testConfig() + + // Test with a bad value + config["shutdown_timeout"] = "this is not good" + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["shutdown_timeout"] = "5s" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_SSHHostPort(t *testing.T) { + var b Builder + config := testConfig() + + // Bad + config["ssh_host_port_min"] = 1000 + config["ssh_host_port_max"] = 500 + b = Builder{} + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Bad + config["ssh_host_port_min"] = -500 + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Good + config["ssh_host_port_min"] = 500 + config["ssh_host_port_max"] = 1000 + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_sshKeyPath(t *testing.T) { + var b Builder + config := testConfig() + + config["ssh_key_path"] = "" + b = Builder{} + err := b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + config["ssh_key_path"] = "/i/dont/exist" + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test bad contents + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(tf.Name()) + defer tf.Close() + + if _, err := tf.Write([]byte("HELLO!")); err != nil { + t.Fatalf("err: %s", err) + } + + config["ssh_key_path"] = tf.Name() + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test good contents + tf.Seek(0, 0) + tf.Truncate(0) + tf.Write([]byte(testPem)) + config["ssh_key_path"] = tf.Name() + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestBuilderPrepare_SSHUser(t *testing.T) { + var b Builder + config := testConfig() + + config["ssh_username"] = "" + b = Builder{} + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + config["ssh_username"] = "exists" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) { + var b Builder + config := testConfig() + + // Test a default boot_wait + delete(config, "ssh_wait_timeout") + err := b.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if b.config.RawSSHWaitTimeout != "20m" { + t.Fatalf("bad value: %s", b.config.RawSSHWaitTimeout) + } + + // Test with a bad value + config["ssh_wait_timeout"] = "this is not good" + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["ssh_wait_timeout"] = "5s" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_QemuArgs(t *testing.T) { + var b Builder + config := testConfig() + + // Test with empty + delete(config, "qemuargs") + err := b.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(b.config.QemuArgs, [][]string{}) { + t.Fatalf("bad: %#v", b.config.QemuArgs) + } + + // Test with a good one + config["qemuargs"] = [][]interface{}{ + []interface{}{"foo", "bar", "baz"}, + } + + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + expected := [][]string{ + []string{"foo", "bar", "baz"}, + } + + if !reflect.DeepEqual(b.config.QemuArgs, expected) { + t.Fatalf("bad: %#v", b.config.QemuArgs) + } +} diff --git a/builder/qemu/driver.go b/builder/qemu/driver.go new file mode 100644 index 000000000..8a8c403e9 --- /dev/null +++ b/builder/qemu/driver.go @@ -0,0 +1,252 @@ +package qemu + +import ( + "bytes" + "errors" + "fmt" + "github.com/mitchellh/multistep" + "log" + "os/exec" + "regexp" + "strings" + "time" +) + +type DriverCancelCallback func(state multistep.StateBag) bool + +// A driver is able to talk to VirtualBox and perform certain +// operations with it. +type Driver interface { + // Initializes the driver with the given values: + // Arguments: qemuPath - string value for the qemu-system-x86_64 executable + // qemuImgPath - string value for the qemu-img executable + Initialize(string, string) + + // Checks if the VM with the given name is running. + IsRunning(string) (bool, error) + + // Stop stops a running machine, forcefully. + Stop(string) error + + // SuppressMessages should do what needs to be done in order to + // suppress any annoying popups from VirtualBox. + SuppressMessages() error + + // Qemu executes the given command via qemu-system-x86_64 + Qemu(vmName string, qemuArgs ...string) error + + // wait on shutdown of the VM with option to cancel + WaitForShutdown( + vmName string, + block bool, + state multistep.StateBag, + cancellCallback DriverCancelCallback) error + + // Qemu executes the given command via qemu-img + QemuImg(...string) error + + // Verify checks to make sure that this driver should function + // properly. If there is any indication the driver can't function, + // this will return an error. + Verify() error + + // Version reads the version of VirtualBox that is installed. + Version() (string, error) +} + +type driverState struct { + cmd *exec.Cmd + cancelChan chan struct{} + waitDone chan error +} + +type QemuDriver struct { + qemuPath string + qemuImgPath string + state map[string]*driverState +} + +func (d *QemuDriver) getDriverState(name string) *driverState { + if _, ok := d.state[name]; !ok { + d.state[name] = &driverState{} + } + return d.state[name] +} + +func (d *QemuDriver) Initialize(qemuPath string, qemuImgPath string) { + d.qemuPath = qemuPath + d.qemuImgPath = qemuImgPath + d.state = make(map[string]*driverState) +} + +func (d *QemuDriver) IsRunning(name string) (bool, error) { + ds := d.getDriverState(name) + return ds.cancelChan != nil, nil +} + +func (d *QemuDriver) Stop(name string) error { + ds := d.getDriverState(name) + + // signal to the command 'wait' to kill the process + if ds.cancelChan != nil { + close(ds.cancelChan) + ds.cancelChan = nil + } + return nil +} + +func (d *QemuDriver) SuppressMessages() error { + return nil +} + +func (d *QemuDriver) Qemu(vmName string, qemuArgs ...string) error { + var stdout, stderr bytes.Buffer + + log.Printf("Executing %s: %#v", d.qemuPath, qemuArgs) + ds := d.getDriverState(vmName) + ds.cmd = exec.Command(d.qemuPath, qemuArgs...) + ds.cmd.Stdout = &stdout + ds.cmd.Stderr = &stderr + + err := ds.cmd.Start() + + if err != nil { + err = fmt.Errorf("Error starting VM: %s", err) + } else { + log.Printf("---- Started Qemu ------- PID = ", ds.cmd.Process.Pid) + + ds.cancelChan = make(chan struct{}) + + // make the channel to watch the process + ds.waitDone = make(chan error) + + // start the virtual machine in the background + go func() { + ds.waitDone <- ds.cmd.Wait() + }() + } + + return err +} + +func (d *QemuDriver) WaitForShutdown(vmName string, + block bool, + state multistep.StateBag, + cancelCallback DriverCancelCallback) error { + var err error + + ds := d.getDriverState(vmName) + + if block { + // wait in the background for completion or caller cancel + for { + select { + case <-ds.cancelChan: + log.Println("Qemu process request to cancel -- killing Qemu process.") + if err = ds.cmd.Process.Kill(); err != nil { + log.Printf("Failed to kill qemu: %v", err) + } + + // clear out the error channel since it's just a cancel + // and therefore the reason for failure is clear + log.Println("Empytying waitDone channel.") + <-ds.waitDone + + // this gig is over -- assure calls to IsRunning see the nil + log.Println("'Nil'ing out cancelChan.") + ds.cancelChan = nil + return errors.New("WaitForShutdown cancelled") + case err = <-ds.waitDone: + log.Printf("Qemu Process done with output = %v", err) + // assure calls to IsRunning see the nil + log.Println("'Nil'ing out cancelChan.") + ds.cancelChan = nil + return nil + case <-time.After(1 * time.Second): + cancel := cancelCallback(state) + if cancel { + log.Println("Qemu process request to cancel -- killing Qemu process.") + + // The step sequence was cancelled, so cancel waiting for SSH + // and just start the halting process. + close(ds.cancelChan) + + log.Println("Cancel request made, quitting waiting for Qemu.") + return errors.New("WaitForShutdown cancelled by interrupt.") + } + } + } + } else { + go func() { + select { + case <-ds.cancelChan: + log.Println("Qemu process request to cancel -- killing Qemu process.") + if err = ds.cmd.Process.Kill(); err != nil { + log.Printf("Failed to kill qemu: %v", err) + } + + // clear out the error channel since it's just a cancel + // and therefore the reason for failure is clear + log.Println("Empytying waitDone channel.") + <-ds.waitDone + log.Println("'Nil'ing out cancelChan.") + ds.cancelChan = nil + + case err = <-ds.waitDone: + log.Printf("Qemu Process done with output = %v", err) + log.Println("'Nil'ing out cancelChan.") + ds.cancelChan = nil + } + }() + } + + ds.cancelChan = nil + return err +} + +func (d *QemuDriver) QemuImg(args ...string) error { + var stdout, stderr bytes.Buffer + + log.Printf("Executing qemu-img: %#v", args) + cmd := exec.Command(d.qemuImgPath, args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + + stdoutString := strings.TrimSpace(stdout.String()) + stderrString := strings.TrimSpace(stderr.String()) + + if _, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf("QemuImg error: %s", stderrString) + } + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + return err +} + +func (d *QemuDriver) Verify() error { + return nil +} + +func (d *QemuDriver) Version() (string, error) { + var stdout bytes.Buffer + + cmd := exec.Command(d.qemuPath, "-version") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + versionOutput := strings.TrimSpace(stdout.String()) + log.Printf("Qemu --version output: %s", versionOutput) + versionRe := regexp.MustCompile("qemu-kvm-[0-9]\\.[0-9]") + matches := versionRe.Split(versionOutput, 2) + if len(matches) == 0 { + return "", fmt.Errorf("No version found: %s", versionOutput) + } + + log.Printf("Qemu version: %s", matches[0]) + return matches[0], nil +} diff --git a/builder/qemu/ssh.go b/builder/qemu/ssh.go new file mode 100644 index 000000000..30eec134e --- /dev/null +++ b/builder/qemu/ssh.go @@ -0,0 +1,59 @@ +package qemu + +import ( + gossh "code.google.com/p/go.crypto/ssh" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/communicator/ssh" + "io/ioutil" + "os" +) + +func sshAddress(state multistep.StateBag) (string, error) { + sshHostPort := state.Get("sshHostPort").(uint) + return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil +} + +func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) { + config := state.Get("config").(*config) + + auth := []gossh.ClientAuth{ + gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)), + gossh.ClientAuthKeyboardInteractive( + ssh.PasswordKeyboardInteractive(config.SSHPassword)), + } + + if config.SSHKeyPath != "" { + keyring, err := sshKeyToKeyring(config.SSHKeyPath) + if err != nil { + return nil, err + } + + auth = append(auth, gossh.ClientAuthKeyring(keyring)) + } + + return &gossh.ClientConfig{ + User: config.SSHUser, + Auth: auth, + }, nil +} + +func sshKeyToKeyring(path string) (gossh.ClientKeyring, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + keyBytes, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + + keyring := new(ssh.SimpleKeychain) + if err := keyring.AddPEMKey(string(keyBytes)); err != nil { + return nil, err + } + + return keyring, nil +} diff --git a/builder/qemu/step_configure_vnc.go b/builder/qemu/step_configure_vnc.go new file mode 100644 index 000000000..cb05b62e1 --- /dev/null +++ b/builder/qemu/step_configure_vnc.go @@ -0,0 +1,53 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "math/rand" + "net" +) + +// This step configures the VM to enable the VNC server. +// +// Uses: +// config *config +// ui packer.Ui +// +// Produces: +// vnc_port uint - The port that VNC is configured to listen on. +type stepConfigureVNC struct{} + +func (stepConfigureVNC) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + ui := state.Get("ui").(packer.Ui) + + // Find an open VNC port. Note that this can still fail later on + // because we have to release the port at some point. But this does its + // best. + msg := fmt.Sprintf("Looking for available port between %d and %d", config.VNCPortMin, config.VNCPortMax) + ui.Say(msg) + log.Printf(msg) + var vncPort uint + portRange := int(config.VNCPortMax - config.VNCPortMin) + for { + vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin + log.Printf("Trying port: %d", vncPort) + l, err := net.Listen("tcp", fmt.Sprintf(":%d", vncPort)) + if err == nil { + defer l.Close() + break + } + } + + msg = fmt.Sprintf("Found available VNC port: %d", vncPort) + ui.Say(msg) + log.Printf(msg) + + state.Put("vnc_port", vncPort) + + return multistep.ActionContinue +} + +func (stepConfigureVNC) Cleanup(multistep.StateBag) {} diff --git a/builder/qemu/step_copy_floppy.go b/builder/qemu/step_copy_floppy.go new file mode 100644 index 000000000..28acfae9f --- /dev/null +++ b/builder/qemu/step_copy_floppy.go @@ -0,0 +1,84 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +// This step attaches the ISO to the virtual machine. +// +// Uses: +// +// Produces: +type stepCopyFloppy struct { + floppyPath string +} + +func (s *stepCopyFloppy) Run(state multistep.StateBag) multistep.StepAction { + // Determine if we even have a floppy disk to attach + var floppyPath string + if floppyPathRaw, ok := state.GetOk("floppy_path"); ok { + floppyPath = floppyPathRaw.(string) + } else { + log.Println("No floppy disk, not attaching.") + return multistep.ActionContinue + } + + // copy the floppy for exclusive use during the vm creation + ui := state.Get("ui").(packer.Ui) + ui.Say("Copying floppy disk for exclusive use...") + floppyPath, err := s.copyFloppy(floppyPath) + if err != nil { + state.Put("error", fmt.Errorf("Error preparing floppy: %s", err)) + return multistep.ActionHalt + } + + // Track the path so that we can remove it later + s.floppyPath = floppyPath + + return multistep.ActionContinue +} + +func (s *stepCopyFloppy) Cleanup(state multistep.StateBag) { + if s.floppyPath == "" { + return + } + + // Delete the floppy disk + ui := state.Get("ui").(packer.Ui) + ui.Say("Removing floppy disk previously copied...") + defer os.Remove(s.floppyPath) +} + +func (s *stepCopyFloppy) copyFloppy(path string) (string, error) { + tempdir, err := ioutil.TempDir("", "packer") + if err != nil { + return "", err + } + + floppyPath := filepath.Join(tempdir, "floppy.img") + f, err := os.Create(floppyPath) + if err != nil { + return "", err + } + defer f.Close() + + sourceF, err := os.Open(path) + if err != nil { + return "", err + } + defer sourceF.Close() + + log.Printf("Copying floppy to temp location: %s", floppyPath) + if _, err := io.Copy(f, sourceF); err != nil { + return "", err + } + + return floppyPath, nil +} diff --git a/builder/qemu/step_create_disk.go b/builder/qemu/step_create_disk.go new file mode 100644 index 000000000..7e6f09b7d --- /dev/null +++ b/builder/qemu/step_create_disk.go @@ -0,0 +1,40 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "path/filepath" + "strings" +) + +// This step creates the virtual disk that will be used as the +// hard drive for the virtual machine. +type stepCreateDisk struct{} + +func (s *stepCreateDisk) Run(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, fmt.Sprintf("%s.%s", config.VMName, + strings.ToLower(config.Format))) + + command := []string{ + "create", + "-f", config.Format, + path, + fmt.Sprintf("%vM", config.DiskSize), + } + + ui.Say("Creating hard drive...") + if err := driver.QemuImg(command...); err != nil { + err := fmt.Errorf("Error creating hard drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_forward_ssh.go b/builder/qemu/step_forward_ssh.go new file mode 100644 index 000000000..7c7925b17 --- /dev/null +++ b/builder/qemu/step_forward_ssh.go @@ -0,0 +1,44 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "math/rand" + "net" +) + +// This step adds a NAT port forwarding definition so that SSH is available +// on the guest machine. +// +// Uses: +// +// Produces: +type stepForwardSSH struct{} + +func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + ui := state.Get("ui").(packer.Ui) + + log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax) + var sshHostPort uint + portRange := int(config.SSHHostPortMax - config.SSHHostPortMin) + for { + sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin + log.Printf("Trying port: %d", sshHostPort) + l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort)) + if err == nil { + defer l.Close() + break + } + } + ui.Say(fmt.Sprintf("Found port for SSH: %d.", sshHostPort)) + + // Save the port we're using so that future steps can use it + state.Put("sshHostPort", sshHostPort) + + return multistep.ActionContinue +} + +func (s *stepForwardSSH) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_http_server.go b/builder/qemu/step_http_server.go new file mode 100644 index 000000000..08a4bd7fa --- /dev/null +++ b/builder/qemu/step_http_server.go @@ -0,0 +1,75 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "math/rand" + "net" + "net/http" +) + +// This step creates and runs the HTTP server that is serving the files +// specified by the 'http_files` configuration parameter in the template. +// +// Uses: +// config *config +// ui packer.Ui +// +// Produces: +// http_port int - The port the HTTP server started on. +type stepHTTPServer struct { + l net.Listener +} + +func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + ui := state.Get("ui").(packer.Ui) + + var httpPort uint = 0 + if config.HTTPDir == "" { + state.Put("http_port", httpPort) + return multistep.ActionContinue + } + + // Find an available TCP port for our HTTP server + var httpAddr string + portRange := int(config.HTTPPortMax - config.HTTPPortMin) + for { + var err error + var offset uint = 0 + + if portRange > 0 { + // Intn will panic if portRange == 0, so we do a check. + offset = uint(rand.Intn(portRange)) + } + + httpPort = offset + config.HTTPPortMin + httpAddr = fmt.Sprintf(":%d", httpPort) + log.Printf("Trying port: %d", httpPort) + s.l, err = net.Listen("tcp", httpAddr) + if err == nil { + break + } + } + + ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort)) + + // Start the HTTP server and run it in the background + fileServer := http.FileServer(http.Dir(config.HTTPDir)) + server := &http.Server{Addr: httpAddr, Handler: fileServer} + go server.Serve(s.l) + + // Save the address into the state so it can be accessed in the future + state.Put("http_port", httpPort) + + return multistep.ActionContinue +} + +func (s *stepHTTPServer) Cleanup(multistep.StateBag) { + if s.l != nil { + // Close the listener so that the HTTP server stops + s.l.Close() + } +} diff --git a/builder/qemu/step_prepare_output_dir.go b/builder/qemu/step_prepare_output_dir.go new file mode 100644 index 000000000..43320399a --- /dev/null +++ b/builder/qemu/step_prepare_output_dir.go @@ -0,0 +1,49 @@ +package qemu + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "os" + "time" +) + +type stepPrepareOutputDir struct{} + +func (stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + ui := state.Get("ui").(packer.Ui) + + if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce { + ui.Say("Deleting previous output directory...") + os.RemoveAll(config.OutputDir) + } + + if err := os.MkdirAll(config.OutputDir, 0755); err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) { + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + + if cancelled || halted { + config := state.Get("config").(*config) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deleting output directory...") + for i := 0; i < 5; i++ { + err := os.RemoveAll(config.OutputDir) + if err == nil { + break + } + + log.Printf("Error removing output dir: %s", err) + time.Sleep(2 * time.Second) + } + } +} diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go new file mode 100644 index 000000000..a5c9473db --- /dev/null +++ b/builder/qemu/step_run.go @@ -0,0 +1,134 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "path/filepath" + "strings" + "time" +) + +type stepRun struct { + vmName string +} + +func runBootCommand(state multistep.StateBag, + actionChannel chan multistep.StepAction) { + config := state.Get("config").(*config) + ui := state.Get("ui").(packer.Ui) + bootCmd := stepTypeBootCommand{} + + if int64(config.bootWait) > 0 { + ui.Say(fmt.Sprintf("Waiting %s for boot...", config.bootWait)) + time.Sleep(config.bootWait) + } + + actionChannel <- bootCmd.Run(state) +} + +func cancelCallback(state multistep.StateBag) bool { + cancel := false + if _, ok := state.GetOk(multistep.StateCancelled); ok { + cancel = true + } + return cancel +} + +func (s *stepRun) runVM( + sendBootCommands bool, + bootDrive string, + state multistep.StateBag) multistep.StepAction { + + config := state.Get("config").(*config) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := config.VMName + + imgPath := filepath.Join(config.OutputDir, + fmt.Sprintf("%s.%s", vmName, strings.ToLower(config.Format))) + isoPath := state.Get("iso_path").(string) + vncPort := state.Get("vnc_port").(uint) + guiArgument := "sdl" + sshHostPort := state.Get("sshHostPort").(uint) + vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900) + + ui.Say("Starting the virtual machine for OS Install...") + if config.Headless == true { + ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" + + "In headless mode, errors during the boot sequence or OS setup\n" + + "won't be easily visible. Use at your own discretion.") + guiArgument = "none" + } + + command := []string{ + "-name", vmName, + "-machine", fmt.Sprintf("type=pc-1.0,accel=%s", config.Accelerator), + "-display", guiArgument, + "-net", "nic,model=virtio", + "-net", "user", + "-drive", fmt.Sprintf("file=%s,if=virtio", imgPath), + "-cdrom", isoPath, + "-boot", bootDrive, + "-m", "512m", + "-redir", fmt.Sprintf("tcp:%v::22", sshHostPort), + "-vnc", vnc, + } + if err := driver.Qemu(vmName, command...); err != nil { + err := fmt.Errorf("Error launching VM: %s", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.vmName = vmName + + // run the boot command after its own timeout + if sendBootCommands { + waitDone := make(chan multistep.StepAction, 1) + go runBootCommand(state, waitDone) + select { + case action := <-waitDone: + if action != multistep.ActionContinue { + // stop the VM in its tracks + driver.Stop(vmName) + return multistep.ActionHalt + } + } + } + + ui.Say("Waiting for VM to shutdown...") + if err := driver.WaitForShutdown(vmName, sendBootCommands, state, cancelCallback); err != nil { + err := fmt.Errorf("Error waiting for initial VM install to shutdown: %s", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepRun) Run(state multistep.StateBag) multistep.StepAction { + // First, the OS install boot + action := s.runVM(true, "d", state) + + if action == multistep.ActionContinue { + // Then the provisioning install + action = s.runVM(false, "c", state) + } + + return action +} + +func (s *stepRun) Cleanup(state multistep.StateBag) { + if s.vmName == "" { + return + } + + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + if running, _ := driver.IsRunning(s.vmName); running { + if err := driver.Stop(s.vmName); err != nil { + ui.Error(fmt.Sprintf("Error shutting down VM: %s", err)) + } + } +} diff --git a/builder/qemu/step_shutdown.go b/builder/qemu/step_shutdown.go new file mode 100644 index 000000000..fd59fec84 --- /dev/null +++ b/builder/qemu/step_shutdown.go @@ -0,0 +1,77 @@ +package qemu + +import ( + "errors" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +// This step shuts down the machine. It first attempts to do so gracefully, +// but ultimately forcefully shuts it down if that fails. +// +// Uses: +// communicator packer.Communicator +// config *config +// driver Driver +// ui packer.Ui +// vmName string +// +// Produces: +// +type stepShutdown struct{} + +func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { + comm := state.Get("communicator").(packer.Communicator) + config := state.Get("config").(*config) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := config.VMName + + if config.ShutdownCommand != "" { + ui.Say("Gracefully halting virtual machine...") + log.Printf("Executing shutdown command: %s", config.ShutdownCommand) + cmd := &packer.RemoteCmd{Command: config.ShutdownCommand} + if err := cmd.StartWithUi(comm, ui); err != nil { + err := fmt.Errorf("Failed to send shutdown command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Wait for the machine to actually shut down + log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout) + shutdownTimer := time.After(config.shutdownTimeout) + for { + running, _ := driver.IsRunning(vmName) + if !running { + break + } + + select { + case <-shutdownTimer: + err := errors.New("Timeout while waiting for machine to shut down.") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + default: + time.Sleep(1 * time.Second) + } + } + } else { + ui.Say("Halting the virtual machine...") + if err := driver.Stop(vmName); err != nil { + err := fmt.Errorf("Error stopping VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + log.Println("VM shut down.") + return multistep.ActionContinue +} + +func (s *stepShutdown) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_suppress_messages.go b/builder/qemu/step_suppress_messages.go new file mode 100644 index 000000000..8aa035a2f --- /dev/null +++ b/builder/qemu/step_suppress_messages.go @@ -0,0 +1,29 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// This step sets some variables in Qemu so that annoying +// pop-up messages don't exist. +type stepSuppressMessages struct{} + +func (stepSuppressMessages) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + log.Println("Suppressing messages in Qemu") + if err := driver.SuppressMessages(); err != nil { + err := fmt.Errorf("Error configuring Qemu to suppress messages: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (stepSuppressMessages) Cleanup(state multistep.StateBag) {} diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go new file mode 100644 index 000000000..c3a6bc354 --- /dev/null +++ b/builder/qemu/step_type_boot_command.go @@ -0,0 +1,175 @@ +package qemu + +import ( + "fmt" + "github.com/mitchellh/go-vnc" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "net" + "strings" + "time" + "unicode" + "unicode/utf8" +) + +const KeyLeftShift uint32 = 0xFFE1 + +type bootCommandTemplateData struct { + HTTPIP string + HTTPPort uint + Name string +} + +// This step "types" the boot command into the VM over VNC. +// +// Uses: +// config *config +// http_port int +// ui packer.Ui +// vnc_port uint +// +// Produces: +// +type stepTypeBootCommand struct{} + +func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + httpPort := state.Get("http_port").(uint) + ui := state.Get("ui").(packer.Ui) + vncPort := state.Get("vnc_port").(uint) + + // Connect to VNC + ui.Say("Connecting to VM via VNC") + nc, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", vncPort)) + if err != nil { + err := fmt.Errorf("Error connecting to VNC: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + defer nc.Close() + + c, err := vnc.Client(nc, &vnc.ClientConfig{Exclusive: true}) + if err != nil { + err := fmt.Errorf("Error handshaking with VNC: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + defer c.Close() + + log.Printf("Connected to VNC desktop: %s", c.DesktopName) + + tplData := &bootCommandTemplateData{ + "127.0.0.1", + httpPort, + config.VMName, + } + + ui.Say("Typing the boot command over VNC...") + for _, command := range config.BootCommand { + command, err := config.tpl.Process(command, tplData) + if err != nil { + err := fmt.Errorf("Error preparing boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Check for interrupts between typing things so we can cancel + // since this isn't the fastest thing. + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return multistep.ActionHalt + } + + vncSendString(c, command) + } + + return multistep.ActionContinue +} + +func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} + +func vncSendString(c *vnc.ClientConn, original string) { + special := make(map[string]uint32) + special[""] = 0xFF08 + special[""] = 0xFFFF + special[""] = 0xFF0D + special[""] = 0xFF1B + special[""] = 0xFFBE + special[""] = 0xFFBF + special[""] = 0xFFC0 + special[""] = 0xFFC1 + special[""] = 0xFFC2 + special[""] = 0xFFC3 + special[""] = 0xFFC4 + special[""] = 0xFFC5 + special[""] = 0xFFC6 + special[""] = 0xFFC7 + special[""] = 0xFFC8 + special[""] = 0xFFC9 + special[""] = 0xFF0D + special[""] = 0xFF09 + + shiftedChars := "~!@#$%^&*()_+{}|:\"<>?" + + // TODO(mitchellh): Ripe for optimizations of some point, perhaps. + for len(original) > 0 { + var keyCode uint32 + keyShift := false + + if strings.HasPrefix(original, "") { + log.Printf("Special code '' found, sleeping one second") + time.Sleep(1 * time.Second) + original = original[len(""):] + continue + } + + if strings.HasPrefix(original, "") { + log.Printf("Special code '' found, sleeping 5 seconds") + time.Sleep(5 * time.Second) + original = original[len(""):] + continue + } + + if strings.HasPrefix(original, "") { + log.Printf("Special code '' found, sleeping 10 seconds") + time.Sleep(10 * time.Second) + original = original[len(""):] + continue + } + + for specialCode, specialValue := range special { + if strings.HasPrefix(original, specialCode) { + log.Printf("Special code '%s' found, replacing with: %d", specialCode, specialValue) + keyCode = specialValue + original = original[len(specialCode):] + break + } + } + + if keyCode == 0 { + r, size := utf8.DecodeRuneInString(original) + original = original[size:] + keyCode = uint32(r) + keyShift = unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r) + + log.Printf("Sending char '%c', code %d, shift %v", r, keyCode, keyShift) + } + + if keyShift { + c.KeyEvent(KeyLeftShift, true) + } + + c.KeyEvent(keyCode, true) + c.KeyEvent(keyCode, false) + + if keyShift { + c.KeyEvent(KeyLeftShift, false) + } + + // qemu is picky, so no matter what, wait a small period + time.Sleep(100 * time.Millisecond) + } +} diff --git a/config.go b/config.go index 9b2c22a44..448912ea5 100644 --- a/config.go +++ b/config.go @@ -24,6 +24,7 @@ const defaultConfig = ` "amazon-instance": "packer-builder-amazon-instance", "digitalocean": "packer-builder-digitalocean", "openstack": "packer-builder-openstack", + "qemu": "packer-builder-qemu", "virtualbox": "packer-builder-virtualbox", "vmware": "packer-builder-vmware" }, diff --git a/plugin/builder-qemu/main.go b/plugin/builder-qemu/main.go new file mode 100644 index 000000000..710742bad --- /dev/null +++ b/plugin/builder-qemu/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/qemu" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + plugin.ServeBuilder(new(qemu.Builder)) +} diff --git a/plugin/builder-qemu/main_test.go b/plugin/builder-qemu/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-qemu/main_test.go @@ -0,0 +1 @@ +package main diff --git a/website/source/docs/builders/qemu.html.markdown b/website/source/docs/builders/qemu.html.markdown new file mode 100644 index 000000000..4d652715d --- /dev/null +++ b/website/source/docs/builders/qemu.html.markdown @@ -0,0 +1,303 @@ +--- +layout: "docs" +--- + +# Qemu (qemu-system-x86_64) Builder + +Type: `qemu` + +The Qemu builder is able to create [KVM](http://www.linux-kvm.org) +and [Xen](http://www.xenproject.org) virtual machine images. Support +for Xen is experimanetal at this time. + +The builder builds a virtual machine by creating a new virtual machine +from scratch, booting it, installing an OS, rebooting the machine with the +boot media as the virtual hard drive, provisioning software within +the OS, then shutting it down. The result of the Qemu builder is a directory +containing the image file necessary to run the virtual machine on KVM or Xen. + +## Basic Example + +Here is a basic example. This example is functional so long as you fixup +paths to files, URLS for ISOs and checksums. + +
+{
+  "builders":
+  [
+    {
+      "type": "qemu",
+      "iso_url": "http://mirror.raystedman.net/centos/6/isos/x86_64/CentOS-6.4-x86_64-minimal.iso",
+      "iso_checksum": "4a5fa01c81cc300f4729136e28ebe600",
+      "iso_checksum_type": "md5",
+      "output_directory": "output_centos_tdhtest",
+      "ssh_wait_timeout": "30s",
+      "shutdown_command": "shutdown -P now",
+      "disk_size": 5000,
+      "format": "qcow2",
+      "headless": false,
+      "accelerator": "kvm",
+      "http_directory": "/home/tdhite/packer/httpfiles",
+      "http_port_min": 10082,
+      "http_port_max": 10089,
+      "ssh_host_port_min": 2222,
+      "ssh_host_port_max": 2229,
+      "ssh_username": "root",
+      "ssh_password": "s0m3password",
+      "ssh_port": 22,
+      "ssh_wait_timeout": "90m",
+      "vm_name": "tdhtest",
+      "boot_command":
+      [
+        "",
+        " ks=http://10.0.2.2:{{ .HTTPPort }}/centos6-ks.cfg"
+      ]
+    }
+  ]
+}
+
+ +The following is a sample CentOS kickstart file you should place in the +ttp_files directory with the name centos6-ks.cfg: + +
+text
+skipx
+install
+url --url http://mirror.raystedman.net/centos/6/os/x86_64/
+repo --name=updates --baseurl=http://mirror.raystedman.net/centos/6/updates/x86_64/
+lang en_US.UTF-8
+keyboard us
+rootpw s0m3password
+firewall --disable
+authconfig --enableshadow --passalgo=sha512
+selinux --disabled
+timezone Etc/UTC
+%include /tmp/kspre.cfg
+
+services --enabled=network,sshd/sendmail
+
+poweroff
+
+%packages --nobase
+at
+acpid
+cronie-noanacron
+crontabs
+logrotate
+mailx
+mlocate
+openssh-clients
+openssh-server
+rsync
+sendmail
+tmpwatch
+vixie-cron
+which
+wget
+yum
+-biosdevname
+-postfix
+-prelink
+%end
+
+%pre
+bootdrive=vda
+
+if [ -f "/dev/$bootdrive" ] ; then
+  exec < /dev/tty3 > /dev/tty3
+  chvt 3
+  echo "ERROR: Drive device does not exist at /dev/$bootdrive!"
+  sleep 5
+  halt -f
+fi
+
+cat >/tmp/kspre.cfg <
+
+## Configuration Reference
+
+There are many configuration options available for the Qemu builder.
+They are organized below into two categories: required and optional. Within
+each category, the available options are alphabetized and described.
+
+Required:
+
+* `iso_checksum` (string) - The checksum for the OS ISO file. Because ISO
+  files are so large, this is required and Packer will verify it prior
+  to booting a virtual machine with the ISO attached. The type of the
+  checksum is specified with `iso_checksum_type`, documented below.
+
+* `iso_checksum_type` (string) - The type of the checksum specified in
+  `iso_checksum`. Valid values are "md5", "sha1", "sha256", or "sha512" currently.
+
+* `iso_url` (string) - A URL to the ISO containing the installation image.
+  This URL can be either an HTTP URL or a file URL (or path to a file).
+  If this is an HTTP URL, Packer will download it and cache it between
+  runs.
+
+* `ssh_username` (string) - The username to use to SSH into the machine
+  once the OS is installed.
+
+Optional:
+
+* `boot_command` (array of strings) - This is an array of commands to type
+  when the virtual machine is first booted. The goal of these commands should
+  be to type just enough to initialize the operating system installer. Special
+  keys can be typed as well, and are covered in the section below on the boot
+  command. If this is not specified, it is assumed the installer will start
+  itself.
+
+* `boot_wait` (string) - The time to wait after booting the initial virtual
+  machine before typing the `boot_command`. The value of this should be
+  a duration. Examples are "5s" and "1m30s" which will cause Packer to wait
+  five seconds and one minute 30 seconds, respectively. If this isn't specified,
+  the default is 10 seconds.
+
+* `disk_size` (int) - The size, in megabytes, of the hard disk to create
+  for the VM. By default, this is 40000 (40 GB).
+
+* `floppy_files` (array of strings) - A list of files to put onto a floppy
+  disk that is attached when the VM is booted for the first time. 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. The files listed in this configuration will all be put
+  into the root directory of the floppy disk; sub-directories are not supported.
+
+* `format` (string) - Either "qcow2" or "img", this specifies the output
+  format of the virtual machine image. This defaults to "qcow2".
+
+* `accelerator` (string) - The accelerator type to use when running the VM.
+  This may have a value of either "kvm" or "xen" and you must have that
+  support in on the machine on which you run the builder.
+
+* `headless` (bool) - Packer defaults to building VirtualBox
+  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.
+
+* `http_directory` (string) - Path to a directory to serve using an HTTP
+  server. The files in this directory will be available over HTTP that will
+  be requestable from the virtual machine. This is useful for hosting
+  kickstart files and so on. By default this is "", which means no HTTP
+  server will be started. The address and port of the HTTP server will be
+  available as variables in `boot_command`. This is covered in more detail
+  below.
+
+* `http_port_min` and `http_port_max` (int) - These are the minimum and
+  maximum port to use for the HTTP server started to serve the `http_directory`.
+  Because Packer often runs in parallel, Packer will choose a randomly available
+  port in this range to run the HTTP server. If you want to force the HTTP
+  server to be on one port, make this minimum and maximum port the same.
+  By default the values are 8000 and 9000, respectively.
+
+* `iso_urls` (array of strings) - Multiple URLs for the ISO to download.
+  Packer will try these in order. If anything goes wrong attempting to download
+  or while downloading a single URL, it will move on to the next. All URLs
+  must point to the same file (same checksum). By default this is empty
+  and `iso_url` is used. Only one of `iso_url` or `iso_urls` can be specified.
+
+* `output_directory` (string) - This is the path to the directory where the
+  resulting virtual machine will be created. This may be relative or absolute.
+  If relative, the path is relative to the working directory when `packer`
+  is executed. This directory must not exist or be empty prior to running the builder.
+  By default this is "output-BUILDNAME" where "BUILDNAME" is the name
+  of the build.
+
+* `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.
+
+* `shutdown_timeout` (string) - The amount of time to wait after executing
+  the `shutdown_command` for the virtual machine to actually shut down.
+  If it doesn't shut down in this time, it is an error. By default, the timeout
+  is "5m", or five minutes.
+
+* `ssh_host_port_min` and `ssh_host_port_max` (uint) - The minimum and
+  maximum port to use for the SSH port on the host machine which is forwarded
+  to the SSH port on the guest machine. Because Packer often runs in parallel,
+  Packer will choose a randomly available port in this range to use as the
+  host port.
+
+* `ssh_key_path` (string) - Path to a private key to use for authenticating
+  with SSH. By default this is not set (key-based auth won't be used).
+  The associated public key is expected to already be configured on the
+  VM being prepared by some other process (kickstart, etc.).
+
+* `ssh_password` (string) - The password for `ssh_username` to use to
+  authenticate with SSH. By default this is the empty string.
+
+* `ssh_port` (int) - The port that SSH will be listening on in the guest
+  virtual machine. By default this is 22. The Qemu builder will map, via
+  port forward, a port on the host machine to the port listed here so
+  machines outside the installing VM can access the VM.
+
+* `ssh_wait_timeout` (string) - The duration to wait for SSH to become
+  available. By default this is "20m", or 20 minutes. Note that this should
+  be quite long since the timer begins as soon as the virtual machine is booted.
+
+* `qemuargs` (array of strings reserved for future use).
+
+* `vm_name` (string) - This is the name of the image (QCOW2 or IMG) file for
+  the new virtual machine, without the file extension. By default this is
+  "packer-BUILDNAME", where "BUILDNAME" is the name of the build.
+
+## Boot Command
+
+The `boot_command` configuration is very important: it specifies the keys
+to type when the virtual machine is first booted in order to start the
+OS installer. This command is typed after `boot_wait`, which gives the
+virtual machine some time to actually load the ISO.
+
+As documented above, the `boot_command` is an array of strings. The
+strings are all typed in sequence. It is an array only to improve readability
+within the template.
+
+The boot command is "typed" character for character over a VNC connection
+to the machine, simulating a human actually typing the keyboard. There are
+a set of special keys available. If these are in your boot command, they
+will be replaced by the proper key:
+
+* `` and `` - Simulates an actual "enter" or "return" keypress.
+
+* `` - Simulates pressing the escape key.
+
+* `` - Simulates pressing the tab key.
+
+* `` `` `` - Adds a 1, 5 or 10 second pause before sending any additional keys. This
+  is useful if you have to generally wait for the UI to update before typing more.
+
+In addition to the special keys, each command to type is treated as a
+[configuration template](/docs/templates/configuration-templates.html).
+The available variables are:
+
+* `HTTPIP` and `HTTPPort` - The IP and port, respectively of an HTTP server
+  that is started serving the directory specified by the `http_directory`
+  configuration parameter. If `http_directory` isn't specified, these will
+  be blank!
+
+Example boot command. This is actually a working boot command used to start
+an CentOS 6.4 installer:
+
+
+"boot_command":
+[
+  "",
+  " ks=http://10.0.2.2:{{ .HTTPPort }}/centos6-ks.cfg"
+]
+
From d58a209b73613fe4d03b6aa3c59781ebca418c28 Mon Sep 17 00:00:00 2001 From: Tom Hite Date: Tue, 3 Sep 2013 10:08:04 -0500 Subject: [PATCH 2/6] added network and disk driver options, also a source comment on the kickstart file in the docs (I can't find the original source). --- builder/qemu/builder.go | 41 ++++++++++++++++++- builder/qemu/step_run.go | 4 +- .../source/docs/builders/qemu.html.markdown | 20 +++++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index c40d4b968..1a7afc4d2 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -14,7 +14,24 @@ import ( "time" ) -const BuilderId = "tdhite.qemu" +const BuilderId = "transcend.qemu" + +var netDevice = map[string]bool{ + "ne2k_pci": true, + "i82551": true, + "i82557b": true, + "i82559er": true, + "rtl8139": true, + "e1000": true, + "pcnet": true, + "virtio": true, +} + +var diskInterface = map[string]bool{ + "ide": true, + "scsi": true, + "virtio": true, +} type Builder struct { config config @@ -48,6 +65,8 @@ type config struct { VNCPortMin uint `mapstructure:"vnc_port_min"` VNCPortMax uint `mapstructure:"vnc_port_max"` VMName string `mapstructure:"vm_name"` + NetDevice string `mapstructure:"net_device"` + DiskInterface string `mapstructure:"disk_interface"` RawBootWait string `mapstructure:"boot_wait"` RawSingleISOUrl string `mapstructure:"iso_url"` @@ -135,6 +154,14 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.Format = "qcow2" } + if b.config.NetDevice == "" { + b.config.NetDevice = "virtio" + } + + if b.config.DiskInterface == "" { + b.config.DiskInterface = "virtio" + } + // Errors templates := map[string]*string{ "http_directory": &b.config.HTTPDir, @@ -151,6 +178,8 @@ func (b *Builder) Prepare(raws ...interface{}) error { "shutdown_timeout": &b.config.RawShutdownTimeout, "ssh_wait_timeout": &b.config.RawSSHWaitTimeout, "accelerator": &b.config.Accelerator, + "net_device": &b.config.NetDevice, + "disk_interface": &b.config.DiskInterface, } for n, ptr := range templates { @@ -198,6 +227,16 @@ func (b *Builder) Prepare(raws ...interface{}) error { errs, errors.New("invalid format, only 'kvm' or 'xen' are allowed")) } + if _, ok := netDevice[b.config.NetDevice]; !ok { + errs = packer.MultiErrorAppend( + errs, errors.New("unrecognized network device type")) + } + + if _, ok := diskInterface[b.config.DiskInterface]; !ok { + errs = packer.MultiErrorAppend( + errs, errors.New("unrecognized disk interface type")) + } + if b.config.HTTPPortMin > b.config.HTTPPortMax { errs = packer.MultiErrorAppend( errs, errors.New("http_port_min must be less than http_port_max")) diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go index a5c9473db..f70c8a109 100644 --- a/builder/qemu/step_run.go +++ b/builder/qemu/step_run.go @@ -65,9 +65,9 @@ func (s *stepRun) runVM( "-name", vmName, "-machine", fmt.Sprintf("type=pc-1.0,accel=%s", config.Accelerator), "-display", guiArgument, - "-net", "nic,model=virtio", + "-net", fmt.Sprintf("nic,model=%s", config.NetDevice), "-net", "user", - "-drive", fmt.Sprintf("file=%s,if=virtio", imgPath), + "-drive", fmt.Sprintf("file=%s,if=%s", imgPath, config.DiskInterface), "-cdrom", isoPath, "-boot", bootDrive, "-m", "512m", diff --git a/website/source/docs/builders/qemu.html.markdown b/website/source/docs/builders/qemu.html.markdown index 4d652715d..60a2323c5 100644 --- a/website/source/docs/builders/qemu.html.markdown +++ b/website/source/docs/builders/qemu.html.markdown @@ -47,6 +47,8 @@ paths to files, URLS for ISOs and checksums. "ssh_port": 22, "ssh_wait_timeout": "90m", "vm_name": "tdhtest", + "net_device": "virtio", + "disk_interface": "virtio", "boot_command": [ "", @@ -57,8 +59,9 @@ paths to files, URLS for ISOs and checksums. }
-The following is a sample CentOS kickstart file you should place in the -ttp_files directory with the name centos6-ks.cfg: +The following is a working CentOS 6.x kickstart file adapted from +an unknown source. You would place such a file in the http_files +directory with the name centos6-ks.cfg:
 text
@@ -172,6 +175,11 @@ Optional:
 * `disk_size` (int) - The size, in megabytes, of the hard disk to create
   for the VM. By default, this is 40000 (40 GB).
 
+* `disk_interface` (string) - The interface to use for the disk. Allowed
+  values include any of "ide," "scsi" or "virtio." Note also that any boot
+  commands or kickstart type scripts must have proper adjustments for
+  resulting device names. The Qemu builder uses "virtio" by default.
+
 * `floppy_files` (array of strings) - A list of files to put onto a floppy
   disk that is attached when the VM is booted for the first time. This is
   most useful for unattended Windows installs, which look for an
@@ -212,6 +220,12 @@ Optional:
   must point to the same file (same checksum). By default this is empty
   and `iso_url` is used. Only one of `iso_url` or `iso_urls` can be specified.
 
+* `net_device` (string) - The driver to use for the network interface. Allowed
+  values "ne2k_pci," "i82551," "i82557b," "i82559er," "rtl8139," "e1000,"
+  "pcnet" or "virtio." The Qemu builder uses "virtio" by default.
+
+* `qemuargs` (array of strings reserved for future use).
+
 * `output_directory` (string) - This is the path to the directory where the
   resulting virtual machine will be created. This may be relative or absolute.
   If relative, the path is relative to the working directory when `packer`
@@ -251,8 +265,6 @@ Optional:
   available. By default this is "20m", or 20 minutes. Note that this should
   be quite long since the timer begins as soon as the virtual machine is booted.
 
-* `qemuargs` (array of strings reserved for future use).
-
 * `vm_name` (string) - This is the name of the image (QCOW2 or IMG) file for
   the new virtual machine, without the file extension. By default this is
   "packer-BUILDNAME", where "BUILDNAME" is the name of the build.

From 29557f36f11c8eb1ba67d7436c8985d4706f0d26 Mon Sep 17 00:00:00 2001
From: Tom Hite 
Date: Tue, 3 Sep 2013 18:13:05 -0500
Subject: [PATCH 3/6] removed a few stray 'VirtualBox' term uses in comments
 and docs.

---
 builder/qemu/artifact.go                        | 2 +-
 builder/qemu/driver.go                          | 6 +++---
 website/source/docs/builders/qemu.html.markdown | 7 +++----
 3 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/builder/qemu/artifact.go b/builder/qemu/artifact.go
index 0fb310482..a3f1f9a46 100644
--- a/builder/qemu/artifact.go
+++ b/builder/qemu/artifact.go
@@ -5,7 +5,7 @@ import (
 	"os"
 )
 
-// Artifact is the result of running the VirtualBox builder, namely a set
+// Artifact is the result of running the Qemu builder, namely a set
 // of files associated with the resulting machine.
 type Artifact struct {
 	dir string
diff --git a/builder/qemu/driver.go b/builder/qemu/driver.go
index 8a8c403e9..2505887a9 100644
--- a/builder/qemu/driver.go
+++ b/builder/qemu/driver.go
@@ -14,7 +14,7 @@ import (
 
 type DriverCancelCallback func(state multistep.StateBag) bool
 
-// A driver is able to talk to VirtualBox and perform certain
+// A driver is able to talk to qemu-system-x86_64 and perform certain
 // operations with it.
 type Driver interface {
 	// Initializes the driver with the given values:
@@ -29,7 +29,7 @@ type Driver interface {
 	Stop(string) error
 
 	// SuppressMessages should do what needs to be done in order to
-	// suppress any annoying popups from VirtualBox.
+	// suppress any annoying popups, if any.
 	SuppressMessages() error
 
 	// Qemu executes the given command via qemu-system-x86_64
@@ -50,7 +50,7 @@ type Driver interface {
 	// this will return an error.
 	Verify() error
 
-	// Version reads the version of VirtualBox that is installed.
+	// Version reads the version of Qemu that is installed.
 	Version() (string, error)
 }
 
diff --git a/website/source/docs/builders/qemu.html.markdown b/website/source/docs/builders/qemu.html.markdown
index 60a2323c5..4b6dc80b3 100644
--- a/website/source/docs/builders/qemu.html.markdown
+++ b/website/source/docs/builders/qemu.html.markdown
@@ -194,10 +194,9 @@ Optional:
   This may have a value of either "kvm" or "xen" and you must have that
   support in on the machine on which you run the builder.
 
-* `headless` (bool) - Packer defaults to building VirtualBox
-  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.
+* `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.
 
 * `http_directory` (string) - Path to a directory to serve using an HTTP
   server. The files in this directory will be available over HTTP that will

From ba1ca4d2fb670fa195f0b2c73c1013205411d543 Mon Sep 17 00:00:00 2001
From: Tom Hite 
Date: Tue, 3 Sep 2013 20:13:45 -0500
Subject: [PATCH 4/6] changed error string referring to 'ova' and 'ovf' to
 refer to 'qcow2' and 'img' as the former were stray leftovers from the
 virtualbox code used as a basis for this plugin.

---
 builder/qemu/builder.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go
index 1a7afc4d2..1568843ae 100644
--- a/builder/qemu/builder.go
+++ b/builder/qemu/builder.go
@@ -219,7 +219,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
 
 	if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
 		errs = packer.MultiErrorAppend(
-			errs, errors.New("invalid format, only 'ovf' or 'ova' are allowed"))
+			errs, errors.New("invalid format, only 'qcow2' or 'img' are allowed"))
 	}
 
 	if !(b.config.Accelerator == "kvm" || b.config.Accelerator == "xen") {

From 2f8f2d5ad1f01dd548636f82fe3462cff55dc56e Mon Sep 17 00:00:00 2001
From: Tom Hite 
Date: Mon, 7 Oct 2013 20:58:08 -0500
Subject: [PATCH 5/6] Fixes #1 and Fixes #2 by allowing qemuargs to operate and
 override defaults.

---
 builder/qemu/builder.go                       |  29 +++--
 builder/qemu/step_run.go                      | 107 +++++++++++++-----
 .../source/docs/builders/qemu.html.markdown   |  33 +++++-
 3 files changed, 132 insertions(+), 37 deletions(-)

diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go
index 1568843ae..64b921906 100644
--- a/builder/qemu/builder.go
+++ b/builder/qemu/builder.go
@@ -17,14 +17,27 @@ import (
 const BuilderId = "transcend.qemu"
 
 var netDevice = map[string]bool{
-	"ne2k_pci": true,
-	"i82551":   true,
-	"i82557b":  true,
-	"i82559er": true,
-	"rtl8139":  true,
-	"e1000":    true,
-	"pcnet":    true,
-	"virtio":   true,
+	"ne2k_pci":   true,
+	"i82551":     true,
+	"i82557b":    true,
+	"i82559er":   true,
+	"rtl8139":    true,
+	"e1000":      true,
+	"pcnet":      true,
+	"virtio":     true,
+	"virtio-net": true,
+	"usb-net":    true,
+	"i82559a":    true,
+	"i82559b":    true,
+	"i82559c":    true,
+	"i82550":     true,
+	"i82562":     true,
+	"i82557a":    true,
+	"i82557c":    true,
+	"i82801":     true,
+	"vmxnet3":    true,
+	"i82558a":    true,
+	"i82558b":    true,
 }
 
 var diskInterface = map[string]bool{
diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go
index f70c8a109..f79590f03 100644
--- a/builder/qemu/step_run.go
+++ b/builder/qemu/step_run.go
@@ -35,6 +35,84 @@ func cancelCallback(state multistep.StateBag) bool {
 	return cancel
 }
 
+func (s *stepRun) getCommandArgs(
+	bootDrive string,
+	state multistep.StateBag) []string {
+
+	ui := state.Get("ui").(packer.Ui)
+	config := state.Get("config").(*config)
+	vmName := config.VMName
+	imgPath := filepath.Join(config.OutputDir,
+		fmt.Sprintf("%s.%s", vmName, strings.ToLower(config.Format)))
+	isoPath := state.Get("iso_path").(string)
+	vncPort := state.Get("vnc_port").(uint)
+	guiArgument := "sdl"
+	sshHostPort := state.Get("sshHostPort").(uint)
+	vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900)
+
+	if config.Headless == true {
+		ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" +
+			"In headless mode, errors during the boot sequence or OS setup\n" +
+			"won't be easily visible. Use at your own discretion.")
+		guiArgument = "none"
+	}
+
+	defaultArgs := make(map[string]string)
+	defaultArgs["-name"] = vmName
+	defaultArgs["-machine"] = fmt.Sprintf("type=pc-1.0,accel=%s", config.Accelerator)
+	defaultArgs["-display"] = guiArgument
+	defaultArgs["-netdev"] = "user,id=user.0"
+	defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
+	defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s", imgPath, config.DiskInterface)
+	defaultArgs["-cdrom"] = isoPath
+	defaultArgs["-boot"] = bootDrive
+	defaultArgs["-m"] = "512m"
+	defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort)
+	defaultArgs["-vnc"] = vnc
+
+	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
+		// switch, just different values, each key in the args hash
+		// will have an array of string values
+		for _, qemuArgs := range config.QemuArgs {
+			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)
+			arg[0] = defaultArgs[key]
+			inArgs[key] = arg
+		}
+	}
+
+	// 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
+}
+
 func (s *stepRun) runVM(
 	sendBootCommands bool,
 	bootDrive string,
@@ -45,35 +123,8 @@ func (s *stepRun) runVM(
 	ui := state.Get("ui").(packer.Ui)
 	vmName := config.VMName
 
-	imgPath := filepath.Join(config.OutputDir,
-		fmt.Sprintf("%s.%s", vmName, strings.ToLower(config.Format)))
-	isoPath := state.Get("iso_path").(string)
-	vncPort := state.Get("vnc_port").(uint)
-	guiArgument := "sdl"
-	sshHostPort := state.Get("sshHostPort").(uint)
-	vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900)
-
 	ui.Say("Starting the virtual machine for OS Install...")
-	if config.Headless == true {
-		ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" +
-			"In headless mode, errors during the boot sequence or OS setup\n" +
-			"won't be easily visible. Use at your own discretion.")
-		guiArgument = "none"
-	}
-
-	command := []string{
-		"-name", vmName,
-		"-machine", fmt.Sprintf("type=pc-1.0,accel=%s", config.Accelerator),
-		"-display", guiArgument,
-		"-net", fmt.Sprintf("nic,model=%s", config.NetDevice),
-		"-net", "user",
-		"-drive", fmt.Sprintf("file=%s,if=%s", imgPath, config.DiskInterface),
-		"-cdrom", isoPath,
-		"-boot", bootDrive,
-		"-m", "512m",
-		"-redir", fmt.Sprintf("tcp:%v::22", sshHostPort),
-		"-vnc", vnc,
-	}
+	command := s.getCommandArgs(bootDrive, state)
 	if err := driver.Qemu(vmName, command...); err != nil {
 		err := fmt.Errorf("Error launching VM: %s", err)
 		ui.Error(err.Error())
diff --git a/website/source/docs/builders/qemu.html.markdown b/website/source/docs/builders/qemu.html.markdown
index 4b6dc80b3..69062752a 100644
--- a/website/source/docs/builders/qemu.html.markdown
+++ b/website/source/docs/builders/qemu.html.markdown
@@ -223,7 +223,38 @@ Optional:
   values "ne2k_pci," "i82551," "i82557b," "i82559er," "rtl8139," "e1000,"
   "pcnet" or "virtio." The Qemu builder uses "virtio" by default.
 
-* `qemuargs` (array of strings reserved for future use).
+* `qemuargs` (array of array of strings) - Allows complete control over
+  the qemu command line (though not, at this time, qemu-img). Each array
+  of strings makes up a command line switch that overrides matching default
+  switch/value pairs. Any value specified as an empty string is ignored.
+  All values after the switch are concatenated with no separater. For instance:
+
+
+  . . .
+  "qemuargs": [
+    [ "-m", "1024m" ],
+    [ "--no-acpi", "" ],
+    [
+       "-netdev",
+      "user,id=mynet0,",
+      "hostfwd=hostip:hostport-guestip:guestport",
+      ""
+    ],
+    [ "-device", "virtio-net,netdev=mynet0" ]
+  ]
+  . . .
+
+ + would produce the following (not including other defaults supplied by the builder and not otherwise conflicting with the qemuargs): + +
+    qemu-system-x86 -m 1024m --no-acpi -netdev user,id=mynet0,hostfwd=hostip:hostport-guestip:guestport -device virtio-net,netdev=mynet0"
+
+ + Note that the qemu command line allows extreme flexibility, so beware of + conflicting arguments causing failures of your run. To see the defaults, + look in the packer.log file and search for the qemu-system-x86 command. The + arguments are all printed for review. * `output_directory` (string) - This is the path to the directory where the resulting virtual machine will be created. This may be relative or absolute. From 5e9b03503159dd184ff12685cae22388a971d57b Mon Sep 17 00:00:00 2001 From: Tom Hite Date: Wed, 9 Oct 2013 07:11:10 -0500 Subject: [PATCH 6/6] Fixes #3 via minor documentation fix and setting default properly (in the net_device template value, virtio is incorrect -- must be virtio-net). --- builder/qemu/builder.go | 2 +- .../source/docs/builders/qemu.html.markdown | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index 64b921906..bce0f99d1 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -168,7 +168,7 @@ func (b *Builder) Prepare(raws ...interface{}) error { } if b.config.NetDevice == "" { - b.config.NetDevice = "virtio" + b.config.NetDevice = "virtio-net" } if b.config.DiskInterface == "" { diff --git a/website/source/docs/builders/qemu.html.markdown b/website/source/docs/builders/qemu.html.markdown index 69062752a..626d01b8b 100644 --- a/website/source/docs/builders/qemu.html.markdown +++ b/website/source/docs/builders/qemu.html.markdown @@ -47,7 +47,7 @@ paths to files, URLS for ISOs and checksums. "ssh_port": 22, "ssh_wait_timeout": "90m", "vm_name": "tdhtest", - "net_device": "virtio", + "net_device": "virtio-net", "disk_interface": "virtio", "boot_command": [ @@ -227,7 +227,16 @@ Optional: the qemu command line (though not, at this time, qemu-img). Each array of strings makes up a command line switch that overrides matching default switch/value pairs. Any value specified as an empty string is ignored. - All values after the switch are concatenated with no separater. For instance: + All values after the switch are concatenated with no separater. + + WARNING: The qemu command line allows extreme flexibility, so beware of + conflicting arguments causing failures of your run. For instance, using + --no-acpi could break the ability to send power signal type commands (e.g., + shutdown -P now) to the virtual machine, thus preventing proper shutdown. To + see the defaults, look in the packer.log file and search for the + qemu-system-x86 command. The arguments are all printed for review. + + The following shows a sample usage:
   . . .
@@ -251,11 +260,6 @@ Optional:
     qemu-system-x86 -m 1024m --no-acpi -netdev user,id=mynet0,hostfwd=hostip:hostport-guestip:guestport -device virtio-net,netdev=mynet0"
 
- Note that the qemu command line allows extreme flexibility, so beware of - conflicting arguments causing failures of your run. To see the defaults, - look in the packer.log file and search for the qemu-system-x86 command. The - arguments are all printed for review. - * `output_directory` (string) - This is the path to the directory where the resulting virtual machine will be created. This may be relative or absolute. If relative, the path is relative to the working directory when `packer`