packer-cn/builder/qemu/builder.go

554 lines
14 KiB
Go

package qemu
import (
"context"
"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"`
CpuCount int `mapstructure:"cpus"`
AdditionalDiskSize []string `mapstructure:"disk_additional_size"`
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"`
MemorySize int `mapstructure:"memory"`
NetDevice string `mapstructure:"net_device"`
OutputDir string `mapstructure:"output_directory"`
QemuArgs [][]string `mapstructure:"qemuargs"`
QemuBinary string `mapstructure:"qemu_binary"`
QMPSocketPath string `mapstructure:"qmp_socket_path"`
ShutdownCommand string `mapstructure:"shutdown_command"`
SSHHostPortMin int `mapstructure:"ssh_host_port_min"`
SSHHostPortMax int `mapstructure:"ssh_host_port_max"`
UseDefaultDisplay bool `mapstructure:"use_default_display"`
VNCBindAddress string `mapstructure:"vnc_bind_address"`
VNCPortMin int `mapstructure:"vnc_port_min"`
VNCPortMax int `mapstructure:"vnc_port_max"`
VNCUsePassword bool `mapstructure:"vnc_use_password"`
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.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.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 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 := 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.SSHHostPortMin < 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("ssh_host_port_min must be positive"))
}
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.VNCUsePassword && 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 warnings, errs
}
return 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,
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,
Label: b.config.FloppyConfig.FloppyLabel,
},
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,
new(stepConfigureQMP),
&stepTypeBootCommand{},
)
if b.config.Comm.Type != "none" {
steps = append(steps,
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost(b.config.Comm.SSHHost),
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("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["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"] = uint64(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
}