Windows Hypervisor Platform (WHPX) is the Windows counterpart to HVF and KVM. It's an operating system provided component that provides virtualization acceleration support. This is kind of the missing counterpart to https://github.com/hashicorp/packer/pull/6193. QEMU 2.12 also added support for WHPX. There's no support for libvirt on Windows so nothing was added in those areas. The popular QEMU for Windows distribution does not have WHPX support built-in for legal reasons as the maintainer does not wish to use or obtain any part of Microsoft's SDK to compile the distribution.
525 lines
13 KiB
Go
525 lines
13 KiB
Go
package qemu
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/hashicorp/packer/common"
|
|
"github.com/hashicorp/packer/common/bootcommand"
|
|
"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 netDevice = map[string]bool{
|
|
"ne2k_pci": true,
|
|
"i82551": true,
|
|
"i82557b": true,
|
|
"i82559er": true,
|
|
"rtl8139": true,
|
|
"e1000": true,
|
|
"pcnet": true,
|
|
"virtio": true,
|
|
"virtio-net": true,
|
|
"virtio-net-pci": true,
|
|
"usb-net": true,
|
|
"i82559a": true,
|
|
"i82559b": true,
|
|
"i82559c": true,
|
|
"i82550": true,
|
|
"i82562": true,
|
|
"i82557a": true,
|
|
"i82557c": true,
|
|
"i82801": true,
|
|
"vmxnet3": true,
|
|
"i82558a": true,
|
|
"i82558b": true,
|
|
}
|
|
|
|
var diskInterface = map[string]bool{
|
|
"ide": true,
|
|
"scsi": true,
|
|
"virtio": true,
|
|
"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"`
|
|
Comm communicator.Config `mapstructure:",squash"`
|
|
common.FloppyConfig `mapstructure:",squash"`
|
|
|
|
ISOSkipCache bool `mapstructure:"iso_skip_cache"`
|
|
Accelerator string `mapstructure:"accelerator"`
|
|
DiskInterface string `mapstructure:"disk_interface"`
|
|
DiskSize uint `mapstructure:"disk_size"`
|
|
DiskCache string `mapstructure:"disk_cache"`
|
|
DiskDiscard string `mapstructure:"disk_discard"`
|
|
DetectZeroes string `mapstructure:"disk_detect_zeroes"`
|
|
SkipCompaction bool `mapstructure:"skip_compaction"`
|
|
DiskCompression bool `mapstructure:"disk_compression"`
|
|
Format string `mapstructure:"format"`
|
|
Headless bool `mapstructure:"headless"`
|
|
DiskImage bool `mapstructure:"disk_image"`
|
|
UseBackingFile bool `mapstructure:"use_backing_file"`
|
|
MachineType string `mapstructure:"machine_type"`
|
|
NetDevice string `mapstructure:"net_device"`
|
|
OutputDir string `mapstructure:"output_directory"`
|
|
QemuArgs [][]string `mapstructure:"qemuargs"`
|
|
QemuBinary string `mapstructure:"qemu_binary"`
|
|
ShutdownCommand string `mapstructure:"shutdown_command"`
|
|
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
|
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
|
UseDefaultDisplay bool `mapstructure:"use_default_display"`
|
|
VNCBindAddress string `mapstructure:"vnc_bind_address"`
|
|
VNCPortMin uint `mapstructure:"vnc_port_min"`
|
|
VNCPortMax uint `mapstructure:"vnc_port_max"`
|
|
VMName string `mapstructure:"vm_name"`
|
|
|
|
// These are deprecated, but we keep them around for BC
|
|
// TODO(@mitchellh): remove
|
|
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
|
|
|
|
// TODO(mitchellh): deprecate
|
|
RunOnce bool `mapstructure:"run_once"`
|
|
|
|
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
|
|
|
|
shutdownTimeout time.Duration ``
|
|
ctx interpolate.Context
|
|
}
|
|
|
|
func (b *Builder) Prepare(raws ...interface{}) ([]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, err
|
|
}
|
|
|
|
var errs *packer.MultiError
|
|
warnings := make([]string, 0)
|
|
|
|
if b.config.DiskSize == 0 {
|
|
b.config.DiskSize = 40960
|
|
}
|
|
|
|
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.SSHHostPortMin == 0 {
|
|
b.config.SSHHostPortMin = 2222
|
|
}
|
|
|
|
if b.config.SSHHostPortMax == 0 {
|
|
b.config.SSHHostPortMax = 4444
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
// TODO: backwards compatibility, write fixer instead
|
|
if b.config.SSHWaitTimeout != 0 {
|
|
b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
|
|
}
|
|
|
|
if b.config.ISOSkipCache {
|
|
b.config.ISOChecksumType = "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)...)
|
|
if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
|
|
errs = packer.MultiErrorAppend(errs, es...)
|
|
}
|
|
|
|
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 _, 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 := netDevice[b.config.NetDevice]; !ok {
|
|
errs = packer.MultiErrorAppend(
|
|
errs, errors.New("unrecognized network device type"))
|
|
}
|
|
|
|
if _, ok := diskInterface[b.config.DiskInterface]; !ok {
|
|
errs = packer.MultiErrorAppend(
|
|
errs, errors.New("unrecognized disk interface type"))
|
|
}
|
|
|
|
if _, 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.RawShutdownTimeout == "" {
|
|
b.config.RawShutdownTimeout = "5m"
|
|
}
|
|
|
|
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
|
|
if err != nil {
|
|
errs = packer.MultiErrorAppend(
|
|
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
|
|
}
|
|
|
|
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
|
|
errs = packer.MultiErrorAppend(
|
|
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
|
}
|
|
|
|
if b.config.VNCPortMin > b.config.VNCPortMax {
|
|
errs = packer.MultiErrorAppend(
|
|
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
|
}
|
|
|
|
if b.config.QemuArgs == nil {
|
|
b.config.QemuArgs = make([][]string, 0)
|
|
}
|
|
|
|
if errs != nil && len(errs.Errors) > 0 {
|
|
return warnings, errs
|
|
}
|
|
|
|
return warnings, nil
|
|
}
|
|
|
|
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
|
// Create the driver that we'll use to communicate with Qemu
|
|
driver, err := b.newDriver(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,
|
|
ChecksumType: b.config.ISOChecksumType,
|
|
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,
|
|
},
|
|
new(stepCreateDisk),
|
|
new(stepCopyDisk),
|
|
new(stepResizeDisk),
|
|
&common.StepHTTPServer{
|
|
HTTPDir: b.config.HTTPDir,
|
|
HTTPPortMin: b.config.HTTPPortMin,
|
|
HTTPPortMax: b.config.HTTPPortMax,
|
|
},
|
|
)
|
|
|
|
if b.config.Comm.Type != "none" {
|
|
steps = append(steps,
|
|
new(stepForwardSSH),
|
|
)
|
|
}
|
|
|
|
steps = append(steps,
|
|
new(stepConfigureVNC),
|
|
steprun,
|
|
&stepTypeBootCommand{},
|
|
)
|
|
|
|
if b.config.Comm.Type != "none" {
|
|
steps = append(steps,
|
|
&communicator.StepConnect{
|
|
Config: &b.config.Comm,
|
|
Host: commHost,
|
|
SSHConfig: b.config.Comm.SSHConfigFunc(),
|
|
SSHPort: commPort,
|
|
WinRMPort: commPort,
|
|
},
|
|
)
|
|
}
|
|
|
|
steps = append(steps,
|
|
new(common.StepProvision),
|
|
)
|
|
|
|
steps = append(steps,
|
|
&common.StepCleanupTempKeys{
|
|
Comm: &b.config.Comm,
|
|
},
|
|
)
|
|
steps = append(steps,
|
|
new(stepShutdown),
|
|
)
|
|
|
|
steps = append(steps,
|
|
new(stepConvertDisk),
|
|
)
|
|
|
|
// Setup the state bag
|
|
state := new(multistep.BasicStateBag)
|
|
state.Put("cache", cache)
|
|
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(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["diskName"] = state.Get("disk_filename").(string)
|
|
artifact.state["diskType"] = b.config.Format
|
|
artifact.state["diskSize"] = uint64(b.config.DiskSize)
|
|
artifact.state["domainType"] = b.config.Accelerator
|
|
|
|
return artifact, nil
|
|
}
|
|
|
|
func (b *Builder) Cancel() {
|
|
if b.runner != nil {
|
|
log.Println("Cancelling the step runner...")
|
|
b.runner.Cancel()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|