diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index 7a293312c..0bace9659 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -11,577 +11,26 @@ import ( "os" "os/exec" "path/filepath" - "regexp" - "runtime" - "strings" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/packer/common" - "github.com/hashicorp/packer/common/bootcommand" - "github.com/hashicorp/packer/common/shutdowncommand" "github.com/hashicorp/packer/helper/communicator" - "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" - "github.com/hashicorp/packer/template/interpolate" ) const BuilderId = "transcend.qemu" -var accels = map[string]struct{}{ - "none": {}, - "kvm": {}, - "tcg": {}, - "xen": {}, - "hax": {}, - "hvf": {}, - "whpx": {}, -} - -var diskInterface = map[string]bool{ - "ide": true, - "scsi": true, - "virtio": true, - "virtio-scsi": true, -} - -var diskCache = map[string]bool{ - "writethrough": true, - "writeback": true, - "none": true, - "unsafe": true, - "directsync": true, -} - -var diskDiscard = map[string]bool{ - "unmap": true, - "ignore": true, -} - -var diskDZeroes = map[string]bool{ - "unmap": true, - "on": true, - "off": true, -} - type Builder struct { config Config runner multistep.Runner } -type Config struct { - common.PackerConfig `mapstructure:",squash"` - common.HTTPConfig `mapstructure:",squash"` - common.ISOConfig `mapstructure:",squash"` - bootcommand.VNCConfig `mapstructure:",squash"` - shutdowncommand.ShutdownConfig `mapstructure:",squash"` - CommConfig CommConfig `mapstructure:",squash"` - common.FloppyConfig `mapstructure:",squash"` - common.CDConfig `mapstructure:",squash"` - // Use iso from provided url. Qemu must support - // curl block device. This defaults to `false`. - ISOSkipCache bool `mapstructure:"iso_skip_cache" required:"false"` - // The accelerator type to use when running the VM. - // This may be `none`, `kvm`, `tcg`, `hax`, `hvf`, `whpx`, or `xen`. The appropriate - // software must have already been installed on your build machine to use the - // accelerator you specified. When no accelerator is specified, Packer will try - // to use `kvm` if it is available but will default to `tcg` otherwise. - // - // ~> The `hax` accelerator has issues attaching CDROM ISOs. This is an - // upstream issue which can be tracked - // [here](https://github.com/intel/haxm/issues/20). - // - // ~> The `hvf` and `whpx` accelerator are new and experimental as of - // [QEMU 2.12.0](https://wiki.qemu.org/ChangeLog/2.12#Host_support). - // You may encounter issues unrelated to Packer when using these. You may need to - // add [ "-global", "virtio-pci.disable-modern=on" ] to `qemuargs` depending on the - // guest operating system. - // - // ~> For `whpx`, note that [Stefan Weil's QEMU for Windows distribution](https://qemu.weilnetz.de/w64/) - // does not include WHPX support and users may need to compile or source a - // build of QEMU for Windows themselves with WHPX support. - Accelerator string `mapstructure:"accelerator" required:"false"` - // Additional disks to create. Uses `vm_name` as the disk name template and - // appends `-#` where `#` is the position in the array. `#` starts at 1 since 0 - // is the default disk. Each string represents the disk image size in bytes. - // Optional suffixes 'k' or 'K' (kilobyte, 1024), 'M' (megabyte, 1024k), 'G' - // (gigabyte, 1024M), 'T' (terabyte, 1024G), 'P' (petabyte, 1024T) and 'E' - // (exabyte, 1024P) are supported. 'b' is ignored. Per qemu-img documentation. - // Each additional disk uses the same disk parameters as the default disk. - // Unset by default. - AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false"` - // The number of cpus to use when building the VM. - // The default is `1` CPU. - CpuCount int `mapstructure:"cpus" required:"false"` - // The interface to use for the disk. Allowed values include any of `ide`, - // `scsi`, `virtio` or `virtio-scsi`^\*. 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. - // - // ^\* Please be aware that use of the `scsi` disk interface has been - // disabled by Red Hat due to a bug described - // [here](https://bugzilla.redhat.com/show_bug.cgi?id=1019220). If you are - // running Qemu on RHEL or a RHEL variant such as CentOS, you *must* choose - // one of the other listed interfaces. Using the `scsi` interface under - // these circumstances will cause the build to fail. - DiskInterface string `mapstructure:"disk_interface" required:"false"` - // The size in bytes of the hard disk of the VM. Suffix with the first - // letter of common byte types. Use "k" or "K" for kilobytes, "M" for - // megabytes, G for gigabytes, and T for terabytes. If no value is provided - // for disk_size, Packer uses a default of `40960M` (40 GB). If a disk_size - // number is provided with no units, Packer will default to Megabytes. - DiskSize string `mapstructure:"disk_size" required:"false"` - // Packer resizes the QCOW2 image using - // qemu-img resize. Set this option to true to disable resizing. - // Defaults to false. - SkipResizeDisk bool `mapstructure:"skip_resize_disk" required:"false"` - // The cache mode to use for disk. Allowed values include any of - // `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By - // default, this is set to `writeback`. - DiskCache string `mapstructure:"disk_cache" required:"false"` - // The discard mode to use for disk. Allowed values - // include any of unmap or ignore. By default, this is set to ignore. - DiskDiscard string `mapstructure:"disk_discard" required:"false"` - // The detect-zeroes mode to use for disk. - // Allowed values include any of unmap, on or off. Defaults to off. - // When the value is "off" we don't set the flag in the qemu command, so that - // Packer still works with old versions of QEMU that don't have this option. - DetectZeroes string `mapstructure:"disk_detect_zeroes" required:"false"` - // Packer compacts the QCOW2 image using - // qemu-img convert. Set this option to true to disable compacting. - // Defaults to false. - SkipCompaction bool `mapstructure:"skip_compaction" required:"false"` - // Apply compression to the QCOW2 disk file - // using qemu-img convert. Defaults to false. - DiskCompression bool `mapstructure:"disk_compression" required:"false"` - // Either `qcow2` or `raw`, this specifies the output format of the virtual - // machine image. This defaults to `qcow2`. - Format string `mapstructure:"format" required:"false"` - // Packer defaults to building QEMU 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. - // - // You can still see the console if you make a note of the VNC display - // number chosen, and then connect using `vncviewer -Shared :` - Headless bool `mapstructure:"headless" required:"false"` - // Packer defaults to building from an ISO file, this parameter controls - // whether the ISO URL supplied is actually a bootable QEMU image. When - // this value is set to `true`, the machine will either clone the source or - // use it as a backing file (if `use_backing_file` is `true`); then, it - // will resize the image according to `disk_size` and boot it. - DiskImage bool `mapstructure:"disk_image" required:"false"` - // Only applicable when disk_image is true - // and format is qcow2, set this option to true to create a new QCOW2 - // file that uses the file located at iso_url as a backing file. The new file - // will only contain blocks that have changed compared to the backing file, so - // enabling this option can significantly reduce disk usage. If true, Packer - // will force the `skip_compaction` also to be true as well to skip disk - // conversion which would render the backing file feature useless. - UseBackingFile bool `mapstructure:"use_backing_file" required:"false"` - // The type of machine emulation to use. Run your qemu binary with the - // flags `-machine help` to list available types for your system. This - // defaults to `pc`. - MachineType string `mapstructure:"machine_type" required:"false"` - // The amount of memory to use when building the VM - // in megabytes. This defaults to 512 megabytes. - MemorySize int `mapstructure:"memory" required:"false"` - // The driver to use for the network interface. Allowed values `ne2k_pci`, - // `i82551`, `i82557b`, `i82559er`, `rtl8139`, `e1000`, `pcnet`, `virtio`, - // `virtio-net`, `virtio-net-pci`, `usb-net`, `i82559a`, `i82559b`, - // `i82559c`, `i82550`, `i82562`, `i82557a`, `i82557c`, `i82801`, - // `vmxnet3`, `i82558a` or `i82558b`. The Qemu builder uses `virtio-net` by - // default. - NetDevice string `mapstructure:"net_device" required:"false"` - // Connects the network to this bridge instead of using the user mode - // networking. - // - // **NB** This bridge must already exist. You can use the `virbr0` bridge - // as created by vagrant-libvirt. - // - // **NB** This will automatically enable the QMP socket (see QMPEnable). - // - // **NB** This only works in Linux based OSes. - NetBridge string `mapstructure:"net_bridge" required:"false"` - // 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. - OutputDir string `mapstructure:"output_directory" required:"false"` - // 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 separator. - // - // ~> **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: - // - // In JSON: - // ```json - // "qemuargs": [ - // [ "-m", "1024M" ], - // [ "--no-acpi", "" ], - // [ - // "-netdev", - // "user,id=mynet0,", - // "hostfwd=hostip:hostport-guestip:guestport", - // "" - // ], - // [ "-device", "virtio-net,netdev=mynet0" ] - // ] - // ``` - // - // In HCL2: - // ```hcl - // 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): - // - // ```text - // qemu-system-x86 -m 1024m --no-acpi -netdev - // user,id=mynet0,hostfwd=hostip:hostport-guestip:guestport -device - // virtio-net,netdev=mynet0" - // ``` - // - // ~> **Windows Users:** [QEMU for Windows](https://qemu.weilnetz.de/) - // builds are available though an environmental variable does need to be - // set for QEMU for Windows to redirect stdout to the console instead of - // stdout.txt. - // - // The following shows the environment variable that needs to be set for - // Windows QEMU support: - // - // ```text - // setx SDL_STDIO_REDIRECT=0 - // ``` - // - // You can also use the `SSHHostPort` template variable to produce a packer - // template that can be invoked by `make` in parallel: - // - // In JSON: - // ```json - // "qemuargs": [ - // [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"], - // [ "-device", "virtio-net,netdev=forward,id=net0"] - // ] - // ``` - // - // In HCL2: - // ```hcl - // qemuargs = [ - // [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"], - // [ "-device", "virtio-net,netdev=forward,id=net0"] - // ] - // - // `make -j 3 my-awesome-packer-templates` spawns 3 packer processes, each - // of which will bind to their own SSH port as determined by each process. - // This will also work with WinRM, just change the port forward in - // `qemuargs` to map to WinRM's default port of `5985` or whatever value - // you have the service set to listen on. - // - // This is a template engine and allows access to the following variables: - // `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`, - // `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}` - QemuArgs [][]string `mapstructure:"qemuargs" required:"false"` - // The name of the Qemu binary to look for. This - // defaults to qemu-system-x86_64, but may need to be changed for - // some platforms. For example qemu-kvm, or qemu-system-i386 may be a - // better choice for some systems. - QemuBinary string `mapstructure:"qemu_binary" required:"false"` - // Enable QMP socket. Location is specified by `qmp_socket_path`. Defaults - // to false. - QMPEnable bool `mapstructure:"qmp_enable" required:"false"` - // QMP Socket Path when `qmp_enable` is true. Defaults to - // `output_directory`/`vm_name`.monitor. - QMPSocketPath string `mapstructure:"qmp_socket_path" required:"false"` - // If true, do not pass a -display option - // to qemu, allowing it to choose the default. This may be needed when running - // under macOS, and getting errors about sdl not being available. - UseDefaultDisplay bool `mapstructure:"use_default_display" required:"false"` - // What QEMU -display option to use. Defaults to gtk, use none to not pass the - // -display option allowing QEMU to choose the default. This may be needed when - // running under macOS, and getting errors about sdl not being available. - Display string `mapstructure:"display" required:"false"` - // The IP address that should be - // binded to for VNC. By default packer will use 127.0.0.1 for this. If you - // wish to bind to all interfaces use 0.0.0.0. - VNCBindAddress string `mapstructure:"vnc_bind_address" required:"false"` - // Whether or not to set a password on the VNC server. This option - // automatically enables the QMP socket. See `qmp_socket_path`. Defaults to - // `false`. - VNCUsePassword bool `mapstructure:"vnc_use_password" required:"false"` - // The minimum and maximum port - // to use for VNC access to the virtual machine. The builder uses VNC to type - // the initial boot_command. Because Packer generally runs in parallel, - // Packer uses a randomly chosen port in this range that appears available. By - // default this is 5900 to 6000. The minimum and maximum ports are inclusive. - VNCPortMin int `mapstructure:"vnc_port_min" required:"false"` - VNCPortMax int `mapstructure:"vnc_port_max"` - // This is the name of the image (QCOW2 or IMG) file for - // the new virtual machine. By default this is packer-BUILDNAME, where - // "BUILDNAME" is the name of the build. Currently, no file extension will be - // used unless it is specified in this option. - VMName string `mapstructure:"vm_name" required:"false"` - // The interface to use for the CDROM device which contains the ISO image. - // Allowed values include any of `ide`, `scsi`, `virtio` or - // `virtio-scsi`. The Qemu builder uses `virtio` by default. - // Some ARM64 images require `virtio-scsi`. - CDROMInterface string `mapstructure:"cdrom_interface" required:"false"` - - // TODO(mitchellh): deprecate - RunOnce bool `mapstructure:"run_once"` - - ctx interpolate.Context -} - func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() } func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { - err := config.Decode(&b.config, &config.DecodeOpts{ - Interpolate: true, - InterpolateContext: &b.config.ctx, - InterpolateFilter: &interpolate.RenderFilter{ - Exclude: []string{ - "boot_command", - "qemuargs", - }, - }, - }, raws...) - if err != nil { - return nil, nil, err - } - - var errs *packer.MultiError - warnings := make([]string, 0) - errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(&b.config.ctx)...) - - if b.config.DiskSize == "" || b.config.DiskSize == "0" { - b.config.DiskSize = "40960M" - } else { - // Make sure supplied disk size is valid - // (digits, plus an optional valid unit character). e.g. 5000, 40G, 1t - re := regexp.MustCompile(`^[\d]+(b|k|m|g|t){0,1}$`) - matched := re.MatchString(strings.ToLower(b.config.DiskSize)) - if !matched { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("Invalid disk size.")) - } else { - // Okay, it's valid -- if it doesn't alreay have a suffix, then - // append "M" as the default unit. - re = regexp.MustCompile(`^[\d]+$`) - matched = re.MatchString(strings.ToLower(b.config.DiskSize)) - if matched { - // Needs M added. - b.config.DiskSize = fmt.Sprintf("%sM", b.config.DiskSize) - } - } - } - - if b.config.DiskCache == "" { - b.config.DiskCache = "writeback" - } - - if b.config.DiskDiscard == "" { - b.config.DiskDiscard = "ignore" - } - - if b.config.DetectZeroes == "" { - b.config.DetectZeroes = "off" - } - - if b.config.Accelerator == "" { - if runtime.GOOS == "windows" { - b.config.Accelerator = "tcg" - } else { - // /dev/kvm is a kernel module that may be loaded if kvm is - // installed and the host supports VT-x extensions. To make sure - // this will actually work we need to os.Open() it. If os.Open fails - // the kernel module was not installed or loaded correctly. - if fp, err := os.Open("/dev/kvm"); err != nil { - b.config.Accelerator = "tcg" - } else { - fp.Close() - b.config.Accelerator = "kvm" - } - } - log.Printf("use detected accelerator: %s", b.config.Accelerator) - } else { - log.Printf("use specified accelerator: %s", b.config.Accelerator) - } - - if b.config.MachineType == "" { - b.config.MachineType = "pc" - } - - if b.config.OutputDir == "" { - b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) - } - - if b.config.QemuBinary == "" { - b.config.QemuBinary = "qemu-system-x86_64" - } - - if b.config.MemorySize < 10 { - log.Printf("MemorySize %d is too small, using default: 512", b.config.MemorySize) - b.config.MemorySize = 512 - } - - if b.config.CpuCount < 1 { - log.Printf("CpuCount %d too small, using default: 1", b.config.CpuCount) - b.config.CpuCount = 1 - } - - if b.config.VNCBindAddress == "" { - b.config.VNCBindAddress = "127.0.0.1" - } - - if b.config.VNCPortMin == 0 { - b.config.VNCPortMin = 5900 - } - - if b.config.VNCPortMax == 0 { - b.config.VNCPortMax = 6000 - } - - if b.config.VMName == "" { - b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) - } - - if b.config.Format == "" { - b.config.Format = "qcow2" - } - - errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.CDConfig.Prepare(&b.config.ctx)...) - errs = packer.MultiErrorAppend(errs, b.config.VNCConfig.Prepare(&b.config.ctx)...) - - if b.config.NetDevice == "" { - b.config.NetDevice = "virtio-net" - } - - if b.config.DiskInterface == "" { - b.config.DiskInterface = "virtio" - } - - if b.config.ISOSkipCache { - b.config.ISOChecksum = "none" - } - isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx) - warnings = append(warnings, isoWarnings...) - errs = packer.MultiErrorAppend(errs, isoErrs...) - - errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...) - commConfigWarnings, es := b.config.CommConfig.Prepare(&b.config.ctx) - if len(es) > 0 { - errs = packer.MultiErrorAppend(errs, es...) - } - warnings = append(warnings, commConfigWarnings...) - - if !(b.config.Format == "qcow2" || b.config.Format == "raw") { - errs = packer.MultiErrorAppend( - errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed")) - } - - if b.config.Format != "qcow2" { - b.config.SkipCompaction = true - b.config.DiskCompression = false - } - - if b.config.UseBackingFile { - b.config.SkipCompaction = true - if !(b.config.DiskImage && b.config.Format == "qcow2") { - errs = packer.MultiErrorAppend( - errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true")) - } - } - - if b.config.DiskImage && len(b.config.AdditionalDiskSize) > 0 { - errs = packer.MultiErrorAppend( - errs, errors.New("disk_additional_size can only be used when disk_image is false")) - } - - if b.config.SkipResizeDisk && !(b.config.DiskImage) { - errs = packer.MultiErrorAppend( - errs, errors.New("skip_resize_disk can only be used when disk_image is true")) - } - - if _, ok := accels[b.config.Accelerator]; !ok { - errs = packer.MultiErrorAppend( - errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed")) - } - - if _, ok := diskInterface[b.config.DiskInterface]; !ok { - errs = packer.MultiErrorAppend( - errs, errors.New("unrecognized disk interface type")) - } - - if _, ok := diskCache[b.config.DiskCache]; !ok { - errs = packer.MultiErrorAppend( - errs, errors.New("unrecognized disk cache type")) - } - - if _, ok := diskDiscard[b.config.DiskDiscard]; !ok { - errs = packer.MultiErrorAppend( - errs, errors.New("unrecognized disk discard type")) - } - - if _, ok := diskDZeroes[b.config.DetectZeroes]; !ok { - errs = packer.MultiErrorAppend( - errs, errors.New("unrecognized disk detect zeroes setting")) - } - - 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)) - } - } - - if b.config.VNCPortMin > b.config.VNCPortMax { - errs = packer.MultiErrorAppend( - errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) - } - - if b.config.NetBridge != "" && runtime.GOOS != "linux" { - errs = packer.MultiErrorAppend( - errs, fmt.Errorf("net_bridge is only supported in Linux based OSes")) - } - - if b.config.NetBridge != "" || b.config.VNCUsePassword { - b.config.QMPEnable = true - } - - if b.config.QMPEnable && b.config.QMPSocketPath == "" { - socketName := fmt.Sprintf("%s.monitor", b.config.VMName) - b.config.QMPSocketPath = filepath.Join(b.config.OutputDir, socketName) - } - - if b.config.QemuArgs == nil { - b.config.QemuArgs = make([][]string, 0) - } - - if errs != nil && len(errs.Errors) > 0 { + warnings, errs := b.config.Prepare(raws...) + if errs != nil { return nil, warnings, errs } diff --git a/builder/qemu/builder_test.go b/builder/qemu/builder_test.go index 79d661e21..2efb1f612 100644 --- a/builder/qemu/builder_test.go +++ b/builder/qemu/builder_test.go @@ -1,56 +1,11 @@ package qemu import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "reflect" "testing" - "time" "github.com/hashicorp/packer/packer" ) -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": "md5:0B0F137F17AC10944716020B018F8126", - "iso_url": "http://www.google.com/", - "ssh_username": "foo", - packer.BuildNameConfigKey: "foo", - } -} - func TestBuilder_ImplementsBuilder(t *testing.T) { var raw interface{} raw = &Builder{} @@ -58,616 +13,3 @@ func TestBuilder_ImplementsBuilder(t *testing.T) { t.Error("Builder must implement builder.") } } - -func TestBuilderPrepare_Defaults(t *testing.T) { - var b Builder - config := testConfig() - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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.CommConfig.HostPortMin != 2222 { - t.Errorf("bad min ssh host port: %d", b.config.CommConfig.HostPortMin) - } - - if b.config.CommConfig.HostPortMax != 4444 { - t.Errorf("bad max ssh host port: %d", b.config.CommConfig.HostPortMax) - } - - if b.config.CommConfig.Comm.SSHPort != 22 { - t.Errorf("bad ssh port: %d", b.config.CommConfig.Comm.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_VNCBindAddress(t *testing.T) { - var b Builder - config := testConfig() - - // Test a default boot_wait - delete(config, "vnc_bind_address") - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("err: %s", err) - } - - if b.config.VNCBindAddress != "127.0.0.1" { - t.Fatalf("bad value: %s", b.config.VNCBindAddress) - } -} - -func TestBuilderPrepare_DiskCompaction(t *testing.T) { - var b Builder - config := testConfig() - - // Bad - config["skip_compaction"] = false - config["disk_compression"] = true - config["format"] = "img" - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - if b.config.SkipCompaction != true { - t.Fatalf("SkipCompaction should be true") - } - if b.config.DiskCompression != false { - t.Fatalf("DiskCompression should be false") - } - - // Good - config["skip_compaction"] = false - config["disk_compression"] = true - config["format"] = "qcow2" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - if b.config.SkipCompaction != false { - t.Fatalf("SkipCompaction should be false") - } - if b.config.DiskCompression != true { - t.Fatalf("DiskCompression should be true") - } -} - -func TestBuilderPrepare_DiskSize(t *testing.T) { - type testcase struct { - InputSize string - OutputSize string - ErrExpected bool - } - - testCases := []testcase{ - {"", "40960M", false}, // not provided - {"12345", "12345M", false}, // no unit given, defaults to M - {"12345x", "12345x", true}, // invalid unit - {"12345T", "12345T", false}, // terabytes - {"12345b", "12345b", false}, // bytes get preserved when set. - {"60000M", "60000M", false}, // Original test case - } - for _, tc := range testCases { - // Set input disk size - var b Builder - config := testConfig() - delete(config, "disk_size") - config["disk_size"] = tc.InputSize - - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if (err == nil) == tc.ErrExpected { - t.Fatalf("bad: error when providing disk size %s; Err expected: %t; err recieved: %v", tc.InputSize, tc.ErrExpected, err) - } - - if b.config.DiskSize != tc.OutputSize { - t.Fatalf("bad size: received: %s but expected %s", b.config.DiskSize, tc.OutputSize) - } - } -} - -func TestBuilderPrepare_AdditionalDiskSize(t *testing.T) { - var b Builder - config := testConfig() - - config["disk_additional_size"] = []string{"1M"} - config["disk_image"] = true - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatalf("should have error") - } - - delete(config, "disk_image") - config["disk_additional_size"] = []string{"1M"} - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - if b.config.AdditionalDiskSize[0] != "1M" { - t.Fatalf("bad size: %s", b.config.AdditionalDiskSize) - } -} - -func TestBuilderPrepare_Format(t *testing.T) { - var b Builder - config := testConfig() - - // Bad - config["format"] = "illegal value" - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Good - config["format"] = "qcow2" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - // Good - config["format"] = "raw" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } -} - -func TestBuilderPrepare_UseBackingFile(t *testing.T) { - var b Builder - config := testConfig() - - config["use_backing_file"] = true - - // Bad: iso_url is not a disk_image - config["disk_image"] = false - config["format"] = "qcow2" - b = Builder{} - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Bad: format is not 'qcow2' - config["disk_image"] = true - config["format"] = "raw" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Good: iso_url is a disk image and format is 'qcow2' - config["disk_image"] = true - config["format"] = "qcow2" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } -} - -func TestBuilderPrepare_SkipResizeDisk(t *testing.T) { - config := testConfig() - config["skip_resize_disk"] = true - config["disk_image"] = false - - b := Builder{} - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Errorf("unexpected warns when calling prepare with skip_resize_disk set to true: %#v", warns) - } - if err == nil { - t.Errorf("setting skip_resize_disk to true when disk_image is false should have error") - } -} - -func TestBuilderPrepare_FloppyFiles(t *testing.T) { - var b Builder - config := testConfig() - - delete(config, "floppy_files") - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("bad err: %s", err) - } - - if len(b.config.FloppyFiles) != 0 { - t.Fatalf("bad: %#v", b.config.FloppyFiles) - } - - floppies_path := "../../common/test-fixtures/floppies" - config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)} - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)} - if !reflect.DeepEqual(b.config.FloppyFiles, expected) { - t.Fatalf("bad: %#v", b.config.FloppyFiles) - } -} - -func TestBuilderPrepare_InvalidFloppies(t *testing.T) { - var b Builder - config := testConfig() - config["floppy_files"] = []string{"nonexistent.bat", "nonexistent.ps1"} - b = Builder{} - _, _, errs := b.Prepare(config) - if errs == nil { - t.Fatalf("Nonexistent floppies should trigger multierror") - } - - if len(errs.(*packer.MultiError).Errors) != 2 { - t.Fatalf("Multierror should work and report 2 errors") - } -} - -func TestBuilderPrepare_InvalidKey(t *testing.T) { - var b Builder - config := testConfig() - - // Add a random key - config["i_should_not_be_valid"] = true - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } -} - -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{} - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Test with a good one - config["output_directory"] = "i-hope-i-dont-exist" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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" - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Test with a good one - config["shutdown_timeout"] = "5s" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } -} - -func TestBuilderPrepare_SSHHostPort(t *testing.T) { - var b Builder - config := testConfig() - - // Bad - config["host_port_min"] = 1000 - config["host_port_max"] = 500 - b = Builder{} - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Bad - config["host_port_min"] = -500 - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Good - config["host_port_min"] = 500 - config["host_port_max"] = 1000 - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } -} - -func TestBuilderPrepare_SSHPrivateKey(t *testing.T) { - var b Builder - config := testConfig() - - config["ssh_private_key_file"] = "" - b = Builder{} - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - config["ssh_private_key_file"] = "/i/dont/exist" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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_private_key_file"] = tf.Name() - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Test good contents - tf.Seek(0, 0) - tf.Truncate(0) - tf.Write([]byte(testPem)) - config["ssh_private_key_file"] = tf.Name() - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) { - var b Builder - config := testConfig() - - // Test a default boot_wait - delete(config, "ssh_timeout") - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("err: %s", err) - } - - // Test with a bad value - config["ssh_timeout"] = "this is not good" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err == nil { - t.Fatal("should have error") - } - - // Test with a good one - config["ssh_timeout"] = "5s" - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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") - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - 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{}{ - {"foo", "bar", "baz"}, - } - - b = Builder{} - _, warns, err = b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - expected := [][]string{ - {"foo", "bar", "baz"}, - } - - if !reflect.DeepEqual(b.config.QemuArgs, expected) { - t.Fatalf("bad: %#v", b.config.QemuArgs) - } -} - -func TestBuilderPrepare_VNCPassword(t *testing.T) { - var b Builder - config := testConfig() - - config["vnc_use_password"] = true - config["output_directory"] = "not-a-real-directory" - b = Builder{} - _, warns, err := b.Prepare(config) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - expected := filepath.Join("not-a-real-directory", "packer-foo.monitor") - if !reflect.DeepEqual(b.config.QMPSocketPath, expected) { - t.Fatalf("Bad QMP socket Path: %s", b.config.QMPSocketPath) - } -} - -func TestCommConfigPrepare_BackwardsCompatibility(t *testing.T) { - var b Builder - config := testConfig() - hostPortMin := 1234 - hostPortMax := 4321 - sshTimeout := 2 * time.Minute - - config["ssh_wait_timeout"] = sshTimeout - config["ssh_host_port_min"] = hostPortMin - config["ssh_host_port_max"] = hostPortMax - - _, warns, err := b.Prepare(config) - if len(warns) == 0 { - t.Fatalf("should have deprecation warn") - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - if b.config.CommConfig.Comm.SSHTimeout != sshTimeout { - t.Fatalf("SSHTimeout should be %s for backwards compatibility, but it was %s", sshTimeout.String(), b.config.CommConfig.Comm.SSHTimeout.String()) - } - - if b.config.CommConfig.HostPortMin != hostPortMin { - t.Fatalf("HostPortMin should be %d for backwards compatibility, but it was %d", hostPortMin, b.config.CommConfig.HostPortMin) - } - - if b.config.CommConfig.HostPortMax != hostPortMax { - t.Fatalf("HostPortMax should be %d for backwards compatibility, but it was %d", hostPortMax, b.config.CommConfig.HostPortMax) - } -} diff --git a/builder/qemu/config.go b/builder/qemu/config.go new file mode 100644 index 000000000..66fb1c3f0 --- /dev/null +++ b/builder/qemu/config.go @@ -0,0 +1,575 @@ +package qemu + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/common/shutdowncommand" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" +) + +var accels = map[string]struct{}{ + "none": {}, + "kvm": {}, + "tcg": {}, + "xen": {}, + "hax": {}, + "hvf": {}, + "whpx": {}, +} + +var diskInterface = map[string]bool{ + "ide": true, + "scsi": true, + "virtio": true, + "virtio-scsi": true, +} + +var diskCache = map[string]bool{ + "writethrough": true, + "writeback": true, + "none": true, + "unsafe": true, + "directsync": true, +} + +var diskDiscard = map[string]bool{ + "unmap": true, + "ignore": true, +} + +var diskDZeroes = map[string]bool{ + "unmap": true, + "on": true, + "off": true, +} + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + common.HTTPConfig `mapstructure:",squash"` + common.ISOConfig `mapstructure:",squash"` + bootcommand.VNCConfig `mapstructure:",squash"` + shutdowncommand.ShutdownConfig `mapstructure:",squash"` + CommConfig CommConfig `mapstructure:",squash"` + common.FloppyConfig `mapstructure:",squash"` + common.CDConfig `mapstructure:",squash"` + // Use iso from provided url. Qemu must support + // curl block device. This defaults to `false`. + ISOSkipCache bool `mapstructure:"iso_skip_cache" required:"false"` + // The accelerator type to use when running the VM. + // This may be `none`, `kvm`, `tcg`, `hax`, `hvf`, `whpx`, or `xen`. The appropriate + // software must have already been installed on your build machine to use the + // accelerator you specified. When no accelerator is specified, Packer will try + // to use `kvm` if it is available but will default to `tcg` otherwise. + // + // ~> The `hax` accelerator has issues attaching CDROM ISOs. This is an + // upstream issue which can be tracked + // [here](https://github.com/intel/haxm/issues/20). + // + // ~> The `hvf` and `whpx` accelerator are new and experimental as of + // [QEMU 2.12.0](https://wiki.qemu.org/ChangeLog/2.12#Host_support). + // You may encounter issues unrelated to Packer when using these. You may need to + // add [ "-global", "virtio-pci.disable-modern=on" ] to `qemuargs` depending on the + // guest operating system. + // + // ~> For `whpx`, note that [Stefan Weil's QEMU for Windows distribution](https://qemu.weilnetz.de/w64/) + // does not include WHPX support and users may need to compile or source a + // build of QEMU for Windows themselves with WHPX support. + Accelerator string `mapstructure:"accelerator" required:"false"` + // Additional disks to create. Uses `vm_name` as the disk name template and + // appends `-#` where `#` is the position in the array. `#` starts at 1 since 0 + // is the default disk. Each string represents the disk image size in bytes. + // Optional suffixes 'k' or 'K' (kilobyte, 1024), 'M' (megabyte, 1024k), 'G' + // (gigabyte, 1024M), 'T' (terabyte, 1024G), 'P' (petabyte, 1024T) and 'E' + // (exabyte, 1024P) are supported. 'b' is ignored. Per qemu-img documentation. + // Each additional disk uses the same disk parameters as the default disk. + // Unset by default. + AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false"` + // The number of cpus to use when building the VM. + // The default is `1` CPU. + CpuCount int `mapstructure:"cpus" required:"false"` + // The interface to use for the disk. Allowed values include any of `ide`, + // `scsi`, `virtio` or `virtio-scsi`^\*. 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. + // + // ^\* Please be aware that use of the `scsi` disk interface has been + // disabled by Red Hat due to a bug described + // [here](https://bugzilla.redhat.com/show_bug.cgi?id=1019220). If you are + // running Qemu on RHEL or a RHEL variant such as CentOS, you *must* choose + // one of the other listed interfaces. Using the `scsi` interface under + // these circumstances will cause the build to fail. + DiskInterface string `mapstructure:"disk_interface" required:"false"` + // The size in bytes of the hard disk of the VM. Suffix with the first + // letter of common byte types. Use "k" or "K" for kilobytes, "M" for + // megabytes, G for gigabytes, and T for terabytes. If no value is provided + // for disk_size, Packer uses a default of `40960M` (40 GB). If a disk_size + // number is provided with no units, Packer will default to Megabytes. + DiskSize string `mapstructure:"disk_size" required:"false"` + // Packer resizes the QCOW2 image using + // qemu-img resize. Set this option to true to disable resizing. + // Defaults to false. + SkipResizeDisk bool `mapstructure:"skip_resize_disk" required:"false"` + // The cache mode to use for disk. Allowed values include any of + // `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By + // default, this is set to `writeback`. + DiskCache string `mapstructure:"disk_cache" required:"false"` + // The discard mode to use for disk. Allowed values + // include any of unmap or ignore. By default, this is set to ignore. + DiskDiscard string `mapstructure:"disk_discard" required:"false"` + // The detect-zeroes mode to use for disk. + // Allowed values include any of unmap, on or off. Defaults to off. + // When the value is "off" we don't set the flag in the qemu command, so that + // Packer still works with old versions of QEMU that don't have this option. + DetectZeroes string `mapstructure:"disk_detect_zeroes" required:"false"` + // Packer compacts the QCOW2 image using + // qemu-img convert. Set this option to true to disable compacting. + // Defaults to false. + SkipCompaction bool `mapstructure:"skip_compaction" required:"false"` + // Apply compression to the QCOW2 disk file + // using qemu-img convert. Defaults to false. + DiskCompression bool `mapstructure:"disk_compression" required:"false"` + // Either `qcow2` or `raw`, this specifies the output format of the virtual + // machine image. This defaults to `qcow2`. + Format string `mapstructure:"format" required:"false"` + // Packer defaults to building QEMU 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. + // + // You can still see the console if you make a note of the VNC display + // number chosen, and then connect using `vncviewer -Shared :` + Headless bool `mapstructure:"headless" required:"false"` + // Packer defaults to building from an ISO file, this parameter controls + // whether the ISO URL supplied is actually a bootable QEMU image. When + // this value is set to `true`, the machine will either clone the source or + // use it as a backing file (if `use_backing_file` is `true`); then, it + // will resize the image according to `disk_size` and boot it. + DiskImage bool `mapstructure:"disk_image" required:"false"` + // Only applicable when disk_image is true + // and format is qcow2, set this option to true to create a new QCOW2 + // file that uses the file located at iso_url as a backing file. The new file + // will only contain blocks that have changed compared to the backing file, so + // enabling this option can significantly reduce disk usage. If true, Packer + // will force the `skip_compaction` also to be true as well to skip disk + // conversion which would render the backing file feature useless. + UseBackingFile bool `mapstructure:"use_backing_file" required:"false"` + // The type of machine emulation to use. Run your qemu binary with the + // flags `-machine help` to list available types for your system. This + // defaults to `pc`. + MachineType string `mapstructure:"machine_type" required:"false"` + // The amount of memory to use when building the VM + // in megabytes. This defaults to 512 megabytes. + MemorySize int `mapstructure:"memory" required:"false"` + // The driver to use for the network interface. Allowed values `ne2k_pci`, + // `i82551`, `i82557b`, `i82559er`, `rtl8139`, `e1000`, `pcnet`, `virtio`, + // `virtio-net`, `virtio-net-pci`, `usb-net`, `i82559a`, `i82559b`, + // `i82559c`, `i82550`, `i82562`, `i82557a`, `i82557c`, `i82801`, + // `vmxnet3`, `i82558a` or `i82558b`. The Qemu builder uses `virtio-net` by + // default. + NetDevice string `mapstructure:"net_device" required:"false"` + // Connects the network to this bridge instead of using the user mode + // networking. + // + // **NB** This bridge must already exist. You can use the `virbr0` bridge + // as created by vagrant-libvirt. + // + // **NB** This will automatically enable the QMP socket (see QMPEnable). + // + // **NB** This only works in Linux based OSes. + NetBridge string `mapstructure:"net_bridge" required:"false"` + // 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. + OutputDir string `mapstructure:"output_directory" required:"false"` + // 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 separator. + // + // ~> **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: + // + // In JSON: + // ```json + // "qemuargs": [ + // [ "-m", "1024M" ], + // [ "--no-acpi", "" ], + // [ + // "-netdev", + // "user,id=mynet0,", + // "hostfwd=hostip:hostport-guestip:guestport", + // "" + // ], + // [ "-device", "virtio-net,netdev=mynet0" ] + // ] + // ``` + // + // In HCL2: + // ```hcl + // 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): + // + // ```text + // qemu-system-x86 -m 1024m --no-acpi -netdev + // user,id=mynet0,hostfwd=hostip:hostport-guestip:guestport -device + // virtio-net,netdev=mynet0" + // ``` + // + // ~> **Windows Users:** [QEMU for Windows](https://qemu.weilnetz.de/) + // builds are available though an environmental variable does need to be + // set for QEMU for Windows to redirect stdout to the console instead of + // stdout.txt. + // + // The following shows the environment variable that needs to be set for + // Windows QEMU support: + // + // ```text + // setx SDL_STDIO_REDIRECT=0 + // ``` + // + // You can also use the `SSHHostPort` template variable to produce a packer + // template that can be invoked by `make` in parallel: + // + // In JSON: + // ```json + // "qemuargs": [ + // [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"], + // [ "-device", "virtio-net,netdev=forward,id=net0"] + // ] + // ``` + // + // In HCL2: + // ```hcl + // qemuargs = [ + // [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"], + // [ "-device", "virtio-net,netdev=forward,id=net0"] + // ] + // + // `make -j 3 my-awesome-packer-templates` spawns 3 packer processes, each + // of which will bind to their own SSH port as determined by each process. + // This will also work with WinRM, just change the port forward in + // `qemuargs` to map to WinRM's default port of `5985` or whatever value + // you have the service set to listen on. + // + // This is a template engine and allows access to the following variables: + // `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`, + // `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}` + QemuArgs [][]string `mapstructure:"qemuargs" required:"false"` + // The name of the Qemu binary to look for. This + // defaults to qemu-system-x86_64, but may need to be changed for + // some platforms. For example qemu-kvm, or qemu-system-i386 may be a + // better choice for some systems. + QemuBinary string `mapstructure:"qemu_binary" required:"false"` + // Enable QMP socket. Location is specified by `qmp_socket_path`. Defaults + // to false. + QMPEnable bool `mapstructure:"qmp_enable" required:"false"` + // QMP Socket Path when `qmp_enable` is true. Defaults to + // `output_directory`/`vm_name`.monitor. + QMPSocketPath string `mapstructure:"qmp_socket_path" required:"false"` + // If true, do not pass a -display option + // to qemu, allowing it to choose the default. This may be needed when running + // under macOS, and getting errors about sdl not being available. + UseDefaultDisplay bool `mapstructure:"use_default_display" required:"false"` + // What QEMU -display option to use. Defaults to gtk, use none to not pass the + // -display option allowing QEMU to choose the default. This may be needed when + // running under macOS, and getting errors about sdl not being available. + Display string `mapstructure:"display" required:"false"` + // The IP address that should be + // binded to for VNC. By default packer will use 127.0.0.1 for this. If you + // wish to bind to all interfaces use 0.0.0.0. + VNCBindAddress string `mapstructure:"vnc_bind_address" required:"false"` + // Whether or not to set a password on the VNC server. This option + // automatically enables the QMP socket. See `qmp_socket_path`. Defaults to + // `false`. + VNCUsePassword bool `mapstructure:"vnc_use_password" required:"false"` + // The minimum and maximum port + // to use for VNC access to the virtual machine. The builder uses VNC to type + // the initial boot_command. Because Packer generally runs in parallel, + // Packer uses a randomly chosen port in this range that appears available. By + // default this is 5900 to 6000. The minimum and maximum ports are inclusive. + VNCPortMin int `mapstructure:"vnc_port_min" required:"false"` + VNCPortMax int `mapstructure:"vnc_port_max"` + // This is the name of the image (QCOW2 or IMG) file for + // the new virtual machine. By default this is packer-BUILDNAME, where + // "BUILDNAME" is the name of the build. Currently, no file extension will be + // used unless it is specified in this option. + VMName string `mapstructure:"vm_name" required:"false"` + // The interface to use for the CDROM device which contains the ISO image. + // Allowed values include any of `ide`, `scsi`, `virtio` or + // `virtio-scsi`. The Qemu builder uses `virtio` by default. + // Some ARM64 images require `virtio-scsi`. + CDROMInterface string `mapstructure:"cdrom_interface" required:"false"` + + // TODO(mitchellh): deprecate + RunOnce bool `mapstructure:"run_once"` + + ctx interpolate.Context +} + +func (c *Config) Prepare(raws ...interface{}) ([]string, error) { + err := config.Decode(c, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: &c.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "boot_command", + "qemuargs", + }, + }, + }, raws...) + if err != nil { + return nil, err + } + + // Accumulate any errors and warnings + var errs *packer.MultiError + warnings := make([]string, 0) + + errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...) + + if c.DiskSize == "" || c.DiskSize == "0" { + c.DiskSize = "40960M" + } else { + // Make sure supplied disk size is valid + // (digits, plus an optional valid unit character). e.g. 5000, 40G, 1t + re := regexp.MustCompile(`^[\d]+(b|k|m|g|t){0,1}$`) + matched := re.MatchString(strings.ToLower(c.DiskSize)) + if !matched { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Invalid disk size.")) + } else { + // Okay, it's valid -- if it doesn't alreay have a suffix, then + // append "M" as the default unit. + re = regexp.MustCompile(`^[\d]+$`) + matched = re.MatchString(strings.ToLower(c.DiskSize)) + if matched { + // Needs M added. + c.DiskSize = fmt.Sprintf("%sM", c.DiskSize) + } + } + } + + if c.DiskCache == "" { + c.DiskCache = "writeback" + } + + if c.DiskDiscard == "" { + c.DiskDiscard = "ignore" + } + + if c.DetectZeroes == "" { + c.DetectZeroes = "off" + } + + if c.Accelerator == "" { + if runtime.GOOS == "windows" { + c.Accelerator = "tcg" + } else { + // /dev/kvm is a kernel module that may be loaded if kvm is + // installed and the host supports VT-x extensions. To make sure + // this will actually work we need to os.Open() it. If os.Open fails + // the kernel module was not installed or loaded correctly. + if fp, err := os.Open("/dev/kvm"); err != nil { + c.Accelerator = "tcg" + } else { + fp.Close() + c.Accelerator = "kvm" + } + } + log.Printf("use detected accelerator: %s", c.Accelerator) + } else { + log.Printf("use specified accelerator: %s", c.Accelerator) + } + + if c.MachineType == "" { + c.MachineType = "pc" + } + + if c.OutputDir == "" { + c.OutputDir = fmt.Sprintf("output-%s", c.PackerBuildName) + } + + if c.QemuBinary == "" { + c.QemuBinary = "qemu-system-x86_64" + } + + if c.MemorySize < 10 { + log.Printf("MemorySize %d is too small, using default: 512", c.MemorySize) + c.MemorySize = 512 + } + + if c.CpuCount < 1 { + log.Printf("CpuCount %d too small, using default: 1", c.CpuCount) + c.CpuCount = 1 + } + + if c.VNCBindAddress == "" { + c.VNCBindAddress = "127.0.0.1" + } + + if c.VNCPortMin == 0 { + c.VNCPortMin = 5900 + } + + if c.VNCPortMax == 0 { + c.VNCPortMax = 6000 + } + + if c.VMName == "" { + c.VMName = fmt.Sprintf("packer-%s", c.PackerBuildName) + } + + if c.Format == "" { + c.Format = "qcow2" + } + + errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...) + + if c.NetDevice == "" { + c.NetDevice = "virtio-net" + } + + if c.DiskInterface == "" { + c.DiskInterface = "virtio" + } + + if c.ISOSkipCache { + c.ISOChecksum = "none" + } + isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx) + warnings = append(warnings, isoWarnings...) + errs = packer.MultiErrorAppend(errs, isoErrs...) + + errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) + commConfigWarnings, es := c.CommConfig.Prepare(&c.ctx) + if len(es) > 0 { + errs = packer.MultiErrorAppend(errs, es...) + } + warnings = append(warnings, commConfigWarnings...) + + if !(c.Format == "qcow2" || c.Format == "raw") { + errs = packer.MultiErrorAppend( + errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed")) + } + + if c.Format != "qcow2" { + c.SkipCompaction = true + c.DiskCompression = false + } + + if c.UseBackingFile { + c.SkipCompaction = true + if !(c.DiskImage && c.Format == "qcow2") { + errs = packer.MultiErrorAppend( + errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true")) + } + } + + if c.DiskImage && len(c.AdditionalDiskSize) > 0 { + errs = packer.MultiErrorAppend( + errs, errors.New("disk_additional_size can only be used when disk_image is false")) + } + + if c.SkipResizeDisk && !(c.DiskImage) { + errs = packer.MultiErrorAppend( + errs, errors.New("skip_resize_disk can only be used when disk_image is true")) + } + + if _, ok := accels[c.Accelerator]; !ok { + errs = packer.MultiErrorAppend( + errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed")) + } + + if _, ok := diskInterface[c.DiskInterface]; !ok { + errs = packer.MultiErrorAppend( + errs, errors.New("unrecognized disk interface type")) + } + + if _, ok := diskCache[c.DiskCache]; !ok { + errs = packer.MultiErrorAppend( + errs, errors.New("unrecognized disk cache type")) + } + + if _, ok := diskDiscard[c.DiskDiscard]; !ok { + errs = packer.MultiErrorAppend( + errs, errors.New("unrecognized disk discard type")) + } + + if _, ok := diskDZeroes[c.DetectZeroes]; !ok { + errs = packer.MultiErrorAppend( + errs, errors.New("unrecognized disk detect zeroes setting")) + } + + if !c.PackerForce { + if _, err := os.Stat(c.OutputDir); err == nil { + errs = packer.MultiErrorAppend( + errs, + fmt.Errorf("Output directory '%s' already exists. It must not exist.", c.OutputDir)) + } + } + + if c.VNCPortMin > c.VNCPortMax { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) + } + + if c.NetBridge != "" && runtime.GOOS != "linux" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("net_bridge is only supported in Linux based OSes")) + } + + if c.NetBridge != "" || c.VNCUsePassword { + c.QMPEnable = true + } + + if c.QMPEnable && c.QMPSocketPath == "" { + socketName := fmt.Sprintf("%s.monitor", c.VMName) + c.QMPSocketPath = filepath.Join(c.OutputDir, socketName) + } + + if c.QemuArgs == nil { + c.QemuArgs = make([][]string, 0) + } + + if errs != nil && len(errs.Errors) > 0 { + return warnings, errs + } + + return warnings, nil + +} diff --git a/builder/qemu/config_test.go b/builder/qemu/config_test.go new file mode 100644 index 000000000..9ca3155cd --- /dev/null +++ b/builder/qemu/config_test.go @@ -0,0 +1,664 @@ +package qemu + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/hashicorp/packer/packer" +) + +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": "md5:0B0F137F17AC10944716020B018F8126", + "iso_url": "http://www.google.com/", + "ssh_username": "foo", + packer.BuildNameConfigKey: "foo", + } +} + +func TestBuilderPrepare_Defaults(t *testing.T) { + var c Config + config := testConfig() + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if c.OutputDir != "output-foo" { + t.Errorf("bad output dir: %s", c.OutputDir) + } + + if c.CommConfig.HostPortMin != 2222 { + t.Errorf("bad min ssh host port: %d", c.CommConfig.HostPortMin) + } + + if c.CommConfig.HostPortMax != 4444 { + t.Errorf("bad max ssh host port: %d", c.CommConfig.HostPortMax) + } + + if c.CommConfig.Comm.SSHPort != 22 { + t.Errorf("bad ssh port: %d", c.CommConfig.Comm.SSHPort) + } + + if c.VMName != "packer-foo" { + t.Errorf("bad vm name: %s", c.VMName) + } + + if c.Format != "qcow2" { + t.Errorf("bad format: %s", c.Format) + } +} + +func TestBuilderPrepare_VNCBindAddress(t *testing.T) { + var c Config + config := testConfig() + + // Test a default boot_wait + delete(config, "vnc_bind_address") + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + if c.VNCBindAddress != "127.0.0.1" { + t.Fatalf("bad value: %s", c.VNCBindAddress) + } +} + +func TestBuilderPrepare_DiskCompaction(t *testing.T) { + var c Config + config := testConfig() + + // Bad + config["skip_compaction"] = false + config["disk_compression"] = true + config["format"] = "img" + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + if c.SkipCompaction != true { + t.Fatalf("SkipCompaction should be true") + } + if c.DiskCompression != false { + t.Fatalf("DiskCompression should be false") + } + + // Good + config["skip_compaction"] = false + config["disk_compression"] = true + config["format"] = "qcow2" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + if c.SkipCompaction != false { + t.Fatalf("SkipCompaction should be false") + } + if c.DiskCompression != true { + t.Fatalf("DiskCompression should be true") + } +} + +func TestBuilderPrepare_DiskSize(t *testing.T) { + type testcase struct { + InputSize string + OutputSize string + ErrExpected bool + } + + testCases := []testcase{ + {"", "40960M", false}, // not provided + {"12345", "12345M", false}, // no unit given, defaults to M + {"12345x", "12345x", true}, // invalid unit + {"12345T", "12345T", false}, // terabytes + {"12345b", "12345b", false}, // bytes get preserved when set. + {"60000M", "60000M", false}, // Original test case + } + for _, tc := range testCases { + // Set input disk size + var c Config + config := testConfig() + delete(config, "disk_size") + config["disk_size"] = tc.InputSize + + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if (err == nil) == tc.ErrExpected { + t.Fatalf("bad: error when providing disk size %s; Err expected: %t; err recieved: %v", tc.InputSize, tc.ErrExpected, err) + } + + if c.DiskSize != tc.OutputSize { + t.Fatalf("bad size: received: %s but expected %s", c.DiskSize, tc.OutputSize) + } + } +} + +func TestBuilderPrepare_AdditionalDiskSize(t *testing.T) { + var c Config + config := testConfig() + + config["disk_additional_size"] = []string{"1M"} + config["disk_image"] = true + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatalf("should have error") + } + + delete(config, "disk_image") + config["disk_additional_size"] = []string{"1M"} + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if c.AdditionalDiskSize[0] != "1M" { + t.Fatalf("bad size: %s", c.AdditionalDiskSize) + } +} + +func TestBuilderPrepare_Format(t *testing.T) { + var c Config + config := testConfig() + + // Bad + config["format"] = "illegal value" + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Good + config["format"] = "qcow2" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + // Good + config["format"] = "raw" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_UseBackingFile(t *testing.T) { + var c Config + config := testConfig() + + config["use_backing_file"] = true + + // Bad: iso_url is not a disk_image + config["disk_image"] = false + config["format"] = "qcow2" + c = Config{} + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Bad: format is not 'qcow2' + config["disk_image"] = true + config["format"] = "raw" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Good: iso_url is a disk image and format is 'qcow2' + config["disk_image"] = true + config["format"] = "qcow2" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_SkipResizeDisk(t *testing.T) { + config := testConfig() + config["skip_resize_disk"] = true + config["disk_image"] = false + + var c Config + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Errorf("unexpected warns when calling prepare with skip_resize_disk set to true: %#v", warns) + } + if err == nil { + t.Errorf("setting skip_resize_disk to true when disk_image is false should have error") + } +} + +func TestBuilderPrepare_FloppyFiles(t *testing.T) { + var c Config + config := testConfig() + + delete(config, "floppy_files") + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if len(c.FloppyFiles) != 0 { + t.Fatalf("bad: %#v", c.FloppyFiles) + } + + floppies_path := "../../common/test-fixtures/floppies" + config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)} + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)} + if !reflect.DeepEqual(c.FloppyFiles, expected) { + t.Fatalf("bad: %#v", c.FloppyFiles) + } +} + +func TestBuilderPrepare_InvalidFloppies(t *testing.T) { + var c Config + config := testConfig() + config["floppy_files"] = []string{"nonexistent.bat", "nonexistent.ps1"} + c = Config{} + _, errs := c.Prepare(config) + if errs == nil { + t.Fatalf("Nonexistent floppies should trigger multierror") + } + + if len(errs.(*packer.MultiError).Errors) != 2 { + t.Fatalf("Multierror should work and report 2 errors") + } +} + +func TestBuilderPrepare_InvalidKey(t *testing.T) { + var c Config + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_OutputDir(t *testing.T) { + var c Config + 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 + c = Config{} + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["output_directory"] = "i-hope-i-dont-exist" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_ShutdownTimeout(t *testing.T) { + var c Config + config := testConfig() + + // Test with a bad value + config["shutdown_timeout"] = "this is not good" + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["shutdown_timeout"] = "5s" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_SSHHostPort(t *testing.T) { + var c Config + config := testConfig() + + // Bad + config["host_port_min"] = 1000 + config["host_port_max"] = 500 + c = Config{} + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Bad + config["host_port_min"] = -500 + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Good + config["host_port_min"] = 500 + config["host_port_max"] = 1000 + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_SSHPrivateKey(t *testing.T) { + var c Config + config := testConfig() + + config["ssh_private_key_file"] = "" + c = Config{} + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + config["ssh_private_key_file"] = "/i/dont/exist" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + 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_private_key_file"] = tf.Name() + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test good contents + tf.Seek(0, 0) + tf.Truncate(0) + tf.Write([]byte(testPem)) + config["ssh_private_key_file"] = tf.Name() + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) { + var c Config + config := testConfig() + + // Test a default boot_wait + delete(config, "ssh_timeout") + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + // Test with a bad value + config["ssh_timeout"] = "this is not good" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + config["ssh_timeout"] = "5s" + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestBuilderPrepare_QemuArgs(t *testing.T) { + var c Config + config := testConfig() + + // Test with empty + delete(config, "qemuargs") + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(c.QemuArgs, [][]string{}) { + t.Fatalf("bad: %#v", c.QemuArgs) + } + + // Test with a good one + config["qemuargs"] = [][]interface{}{ + {"foo", "bar", "baz"}, + } + + c = Config{} + warns, err = c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + expected := [][]string{ + {"foo", "bar", "baz"}, + } + + if !reflect.DeepEqual(c.QemuArgs, expected) { + t.Fatalf("bad: %#v", c.QemuArgs) + } +} + +func TestBuilderPrepare_VNCPassword(t *testing.T) { + var c Config + config := testConfig() + config["vnc_use_password"] = true + config["output_directory"] = "not-a-real-directory" + + warns, err := c.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + expected := filepath.Join("not-a-real-directory", "packer-foo.monitor") + if !reflect.DeepEqual(c.QMPSocketPath, expected) { + t.Fatalf("Bad QMP socket Path: %s", c.QMPSocketPath) + } +} + +func TestCommConfigPrepare_BackwardsCompatibility(t *testing.T) { + var c Config + config := testConfig() + hostPortMin := 1234 + hostPortMax := 4321 + sshTimeout := 2 * time.Minute + + config["ssh_wait_timeout"] = sshTimeout + config["ssh_host_port_min"] = hostPortMin + config["ssh_host_port_max"] = hostPortMax + + warns, err := c.Prepare(config) + if len(warns) == 0 { + t.Fatalf("should have deprecation warn") + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if c.CommConfig.Comm.SSHTimeout != sshTimeout { + t.Fatalf("SSHTimeout should be %s for backwards compatibility, but it was %s", sshTimeout.String(), c.CommConfig.Comm.SSHTimeout.String()) + } + + if c.CommConfig.HostPortMin != hostPortMin { + t.Fatalf("HostPortMin should be %d for backwards compatibility, but it was %d", hostPortMin, c.CommConfig.HostPortMin) + } + + if c.CommConfig.HostPortMax != hostPortMax { + t.Fatalf("HostPortMax should be %d for backwards compatibility, but it was %d", hostPortMax, c.CommConfig.HostPortMax) + } +}