//go:generate struct-markdown //go:generate mapstructure-to-hcl2 -type Config package qemu import ( "context" "errors" "fmt" "log" "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"` // 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"` // 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. 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: // // ```json //{ // "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: // // ```json //{ // "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"` // 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.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.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 _, 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 { return nil, warnings, errs } return nil, warnings, nil } func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { // Create the driver that we'll use to communicate with Qemu driver, err := b.newDriver(b.config.QemuBinary) if err != nil { return nil, fmt.Errorf("Failed creating Qemu driver: %s", err) } steprun := &stepRun{} if !b.config.DiskImage { steprun.BootDrive = "once=d" steprun.Message = "Starting VM, booting from CD-ROM" } else { steprun.BootDrive = "c" steprun.Message = "Starting VM, booting disk image" } steps := []multistep.Step{} if !b.config.ISOSkipCache { steps = append(steps, &common.StepDownload{ Checksum: b.config.ISOChecksum, Description: "ISO", Extension: b.config.TargetExtension, ResultKey: "iso_path", TargetPath: b.config.TargetPath, Url: b.config.ISOUrls, }, ) } else { steps = append(steps, &stepSetISO{ ResultKey: "iso_path", Url: b.config.ISOUrls, }, ) } steps = append(steps, new(stepPrepareOutputDir), &common.StepCreateFloppy{ Files: b.config.FloppyConfig.FloppyFiles, Directories: b.config.FloppyConfig.FloppyDirectories, Label: b.config.FloppyConfig.FloppyLabel, }, new(stepCreateDisk), new(stepCopyDisk), new(stepResizeDisk), new(stepHTTPIPDiscover), &common.StepHTTPServer{ HTTPDir: b.config.HTTPDir, HTTPPortMin: b.config.HTTPPortMin, HTTPPortMax: b.config.HTTPPortMax, HTTPAddress: b.config.HTTPAddress, }, ) if b.config.CommConfig.Comm.Type != "none" && b.config.NetBridge == "" { steps = append(steps, new(stepPortForward), ) } steps = append(steps, new(stepConfigureVNC), steprun, &stepConfigureQMP{ QMPSocketPath: b.config.QMPSocketPath, }, &stepTypeBootCommand{}, ) if b.config.CommConfig.Comm.Type != "none" && b.config.NetBridge != "" { steps = append(steps, &stepWaitGuestAddress{ timeout: b.config.CommConfig.Comm.SSHTimeout, }, ) } if b.config.CommConfig.Comm.Type != "none" { steps = append(steps, &communicator.StepConnect{ Config: &b.config.CommConfig.Comm, Host: commHost(b.config.CommConfig.Comm.Host()), SSHConfig: b.config.CommConfig.Comm.SSHConfigFunc(), SSHPort: commPort, WinRMPort: commPort, }, ) } steps = append(steps, new(common.StepProvision), ) steps = append(steps, &common.StepCleanupTempKeys{ Comm: &b.config.CommConfig.Comm, }, ) steps = append(steps, new(stepShutdown), ) steps = append(steps, new(stepConvertDisk), ) // Setup the state bag state := new(multistep.BasicStateBag) state.Put("config", &b.config) state.Put("debug", b.config.PackerDebug) state.Put("driver", driver) state.Put("hook", hook) state.Put("ui", ui) // Run b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state) b.runner.Run(ctx, 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 err != nil { return err } if !info.IsDir() { files = append(files, path) } return nil } if err := filepath.Walk(b.config.OutputDir, visit); err != nil { return nil, err } artifact := &Artifact{ dir: b.config.OutputDir, f: files, state: make(map[string]interface{}), } artifact.state["generated_data"] = state.Get("generated_data") artifact.state["diskName"] = b.config.VMName diskpaths, ok := state.Get("qemu_disk_paths").([]string) if ok { artifact.state["diskPaths"] = diskpaths } artifact.state["diskType"] = b.config.Format artifact.state["diskSize"] = b.config.DiskSize artifact.state["domainType"] = b.config.Accelerator return artifact, nil } func (b *Builder) newDriver(qemuBinary string) (Driver, error) { qemuPath, err := exec.LookPath(qemuBinary) 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{ QemuPath: qemuPath, QemuImgPath: qemuImgPath, } if err := driver.Verify(); err != nil { return nil, err } return driver, nil }