diff --git a/builder/qemu/artifact.go b/builder/qemu/artifact.go new file mode 100644 index 000000000..a3f1f9a46 --- /dev/null +++ b/builder/qemu/artifact.go @@ -0,0 +1,33 @@ +package qemu + +import ( + "fmt" + "os" +) + +// Artifact is the result of running the Qemu 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..bce0f99d1 --- /dev/null +++ b/builder/qemu/builder.go @@ -0,0 +1,487 @@ +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 = "transcend.qemu" + +var netDevice = map[string]bool{ + "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{ + "ide": true, + "scsi": true, + "virtio": true, +} + +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"` + NetDevice string `mapstructure:"net_device"` + DiskInterface string `mapstructure:"disk_interface"` + + 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" + } + + if b.config.NetDevice == "" { + b.config.NetDevice = "virtio-net" + } + + if b.config.DiskInterface == "" { + b.config.DiskInterface = "virtio" + } + + // 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, + "net_device": &b.config.NetDevice, + "disk_interface": &b.config.DiskInterface, + } + + 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 'qcow2' or 'img' 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 _, 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")) + } + + 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..2505887a9 --- /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 qemu-system-x86_64 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, if any. + 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 Qemu 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..f79590f03 --- /dev/null +++ b/builder/qemu/step_run.go @@ -0,0 +1,185 @@ +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) 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, + state multistep.StateBag) multistep.StepAction { + + config := state.Get("config").(*config) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := config.VMName + + ui.Say("Starting the virtual machine for OS Install...") + 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()) + 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 b4989ae1a..a6a1abd4f 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..626d01b8b --- /dev/null +++ b/website/source/docs/builders/qemu.html.markdown @@ -0,0 +1,349 @@ +--- +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",
+      "net_device": "virtio-net",
+      "disk_interface": "virtio",
+      "boot_command":
+      [
+        "",
+        " ks=http://10.0.2.2:{{ .HTTPPort }}/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
+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).
+
+* `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
+  `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 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.
+
+* `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 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.
+
+  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:
+
+
+  . . .
+  "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"
+
+ +* `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. + +* `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"
+]
+