github.com/mitchellh/packer@v1.3.2/builder/qemu/builder.go (about)

     1  package qemu
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"runtime"
    11  	"time"
    12  
    13  	"github.com/hashicorp/packer/common"
    14  	"github.com/hashicorp/packer/common/bootcommand"
    15  	"github.com/hashicorp/packer/helper/communicator"
    16  	"github.com/hashicorp/packer/helper/config"
    17  	"github.com/hashicorp/packer/helper/multistep"
    18  	"github.com/hashicorp/packer/packer"
    19  	"github.com/hashicorp/packer/template/interpolate"
    20  )
    21  
    22  const BuilderId = "transcend.qemu"
    23  
    24  var accels = map[string]struct{}{
    25  	"none": {},
    26  	"kvm":  {},
    27  	"tcg":  {},
    28  	"xen":  {},
    29  	"hax":  {},
    30  	"hvf":  {},
    31  }
    32  
    33  var netDevice = map[string]bool{
    34  	"ne2k_pci":       true,
    35  	"i82551":         true,
    36  	"i82557b":        true,
    37  	"i82559er":       true,
    38  	"rtl8139":        true,
    39  	"e1000":          true,
    40  	"pcnet":          true,
    41  	"virtio":         true,
    42  	"virtio-net":     true,
    43  	"virtio-net-pci": true,
    44  	"usb-net":        true,
    45  	"i82559a":        true,
    46  	"i82559b":        true,
    47  	"i82559c":        true,
    48  	"i82550":         true,
    49  	"i82562":         true,
    50  	"i82557a":        true,
    51  	"i82557c":        true,
    52  	"i82801":         true,
    53  	"vmxnet3":        true,
    54  	"i82558a":        true,
    55  	"i82558b":        true,
    56  }
    57  
    58  var diskInterface = map[string]bool{
    59  	"ide":         true,
    60  	"scsi":        true,
    61  	"virtio":      true,
    62  	"virtio-scsi": true,
    63  }
    64  
    65  var diskCache = map[string]bool{
    66  	"writethrough": true,
    67  	"writeback":    true,
    68  	"none":         true,
    69  	"unsafe":       true,
    70  	"directsync":   true,
    71  }
    72  
    73  var diskDiscard = map[string]bool{
    74  	"unmap":  true,
    75  	"ignore": true,
    76  }
    77  
    78  var diskDZeroes = map[string]bool{
    79  	"unmap": true,
    80  	"on":    true,
    81  	"off":   true,
    82  }
    83  
    84  type Builder struct {
    85  	config Config
    86  	runner multistep.Runner
    87  }
    88  
    89  type Config struct {
    90  	common.PackerConfig   `mapstructure:",squash"`
    91  	common.HTTPConfig     `mapstructure:",squash"`
    92  	common.ISOConfig      `mapstructure:",squash"`
    93  	bootcommand.VNCConfig `mapstructure:",squash"`
    94  	Comm                  communicator.Config `mapstructure:",squash"`
    95  	common.FloppyConfig   `mapstructure:",squash"`
    96  
    97  	ISOSkipCache      bool       `mapstructure:"iso_skip_cache"`
    98  	Accelerator       string     `mapstructure:"accelerator"`
    99  	DiskInterface     string     `mapstructure:"disk_interface"`
   100  	DiskSize          uint       `mapstructure:"disk_size"`
   101  	DiskCache         string     `mapstructure:"disk_cache"`
   102  	DiskDiscard       string     `mapstructure:"disk_discard"`
   103  	DetectZeroes      string     `mapstructure:"disk_detect_zeroes"`
   104  	SkipCompaction    bool       `mapstructure:"skip_compaction"`
   105  	DiskCompression   bool       `mapstructure:"disk_compression"`
   106  	Format            string     `mapstructure:"format"`
   107  	Headless          bool       `mapstructure:"headless"`
   108  	DiskImage         bool       `mapstructure:"disk_image"`
   109  	UseBackingFile    bool       `mapstructure:"use_backing_file"`
   110  	MachineType       string     `mapstructure:"machine_type"`
   111  	NetDevice         string     `mapstructure:"net_device"`
   112  	OutputDir         string     `mapstructure:"output_directory"`
   113  	QemuArgs          [][]string `mapstructure:"qemuargs"`
   114  	QemuBinary        string     `mapstructure:"qemu_binary"`
   115  	ShutdownCommand   string     `mapstructure:"shutdown_command"`
   116  	SSHHostPortMin    uint       `mapstructure:"ssh_host_port_min"`
   117  	SSHHostPortMax    uint       `mapstructure:"ssh_host_port_max"`
   118  	UseDefaultDisplay bool       `mapstructure:"use_default_display"`
   119  	VNCBindAddress    string     `mapstructure:"vnc_bind_address"`
   120  	VNCPortMin        uint       `mapstructure:"vnc_port_min"`
   121  	VNCPortMax        uint       `mapstructure:"vnc_port_max"`
   122  	VMName            string     `mapstructure:"vm_name"`
   123  
   124  	// These are deprecated, but we keep them around for BC
   125  	// TODO(@mitchellh): remove
   126  	SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
   127  
   128  	// TODO(mitchellh): deprecate
   129  	RunOnce bool `mapstructure:"run_once"`
   130  
   131  	RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
   132  
   133  	shutdownTimeout time.Duration ``
   134  	ctx             interpolate.Context
   135  }
   136  
   137  func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
   138  	err := config.Decode(&b.config, &config.DecodeOpts{
   139  		Interpolate:        true,
   140  		InterpolateContext: &b.config.ctx,
   141  		InterpolateFilter: &interpolate.RenderFilter{
   142  			Exclude: []string{
   143  				"boot_command",
   144  				"qemuargs",
   145  			},
   146  		},
   147  	}, raws...)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	var errs *packer.MultiError
   153  	warnings := make([]string, 0)
   154  
   155  	if b.config.DiskSize == 0 {
   156  		b.config.DiskSize = 40960
   157  	}
   158  
   159  	if b.config.DiskCache == "" {
   160  		b.config.DiskCache = "writeback"
   161  	}
   162  
   163  	if b.config.DiskDiscard == "" {
   164  		b.config.DiskDiscard = "ignore"
   165  	}
   166  
   167  	if b.config.DetectZeroes == "" {
   168  		b.config.DetectZeroes = "off"
   169  	}
   170  
   171  	if b.config.Accelerator == "" {
   172  		if runtime.GOOS == "windows" {
   173  			b.config.Accelerator = "tcg"
   174  		} else {
   175  			// /dev/kvm is a kernel module that may be loaded if kvm is
   176  			// installed and the host supports VT-x extensions. To make sure
   177  			// this will actually work we need to os.Open() it. If os.Open fails
   178  			// the kernel module was not installed or loaded correctly.
   179  			if fp, err := os.Open("/dev/kvm"); err != nil {
   180  				b.config.Accelerator = "tcg"
   181  			} else {
   182  				fp.Close()
   183  				b.config.Accelerator = "kvm"
   184  			}
   185  		}
   186  		log.Printf("use detected accelerator: %s", b.config.Accelerator)
   187  	} else {
   188  		log.Printf("use specified accelerator: %s", b.config.Accelerator)
   189  	}
   190  
   191  	if b.config.MachineType == "" {
   192  		b.config.MachineType = "pc"
   193  	}
   194  
   195  	if b.config.OutputDir == "" {
   196  		b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
   197  	}
   198  
   199  	if b.config.QemuBinary == "" {
   200  		b.config.QemuBinary = "qemu-system-x86_64"
   201  	}
   202  
   203  	if b.config.SSHHostPortMin == 0 {
   204  		b.config.SSHHostPortMin = 2222
   205  	}
   206  
   207  	if b.config.SSHHostPortMax == 0 {
   208  		b.config.SSHHostPortMax = 4444
   209  	}
   210  
   211  	if b.config.VNCBindAddress == "" {
   212  		b.config.VNCBindAddress = "127.0.0.1"
   213  	}
   214  
   215  	if b.config.VNCPortMin == 0 {
   216  		b.config.VNCPortMin = 5900
   217  	}
   218  
   219  	if b.config.VNCPortMax == 0 {
   220  		b.config.VNCPortMax = 6000
   221  	}
   222  
   223  	if b.config.VMName == "" {
   224  		b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
   225  	}
   226  
   227  	if b.config.Format == "" {
   228  		b.config.Format = "qcow2"
   229  	}
   230  
   231  	errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
   232  	errs = packer.MultiErrorAppend(errs, b.config.VNCConfig.Prepare(&b.config.ctx)...)
   233  
   234  	if b.config.NetDevice == "" {
   235  		b.config.NetDevice = "virtio-net"
   236  	}
   237  
   238  	if b.config.DiskInterface == "" {
   239  		b.config.DiskInterface = "virtio"
   240  	}
   241  
   242  	// TODO: backwards compatibility, write fixer instead
   243  	if b.config.SSHWaitTimeout != 0 {
   244  		b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
   245  	}
   246  
   247  	if b.config.ISOSkipCache {
   248  		b.config.ISOChecksumType = "none"
   249  	}
   250  
   251  	isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx)
   252  	warnings = append(warnings, isoWarnings...)
   253  	errs = packer.MultiErrorAppend(errs, isoErrs...)
   254  
   255  	errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...)
   256  	if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
   257  		errs = packer.MultiErrorAppend(errs, es...)
   258  	}
   259  
   260  	if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
   261  		errs = packer.MultiErrorAppend(
   262  			errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
   263  	}
   264  
   265  	if b.config.Format != "qcow2" {
   266  		b.config.SkipCompaction = true
   267  		b.config.DiskCompression = false
   268  	}
   269  
   270  	if b.config.UseBackingFile && !(b.config.DiskImage && b.config.Format == "qcow2") {
   271  		errs = packer.MultiErrorAppend(
   272  			errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true"))
   273  	}
   274  
   275  	if _, ok := accels[b.config.Accelerator]; !ok {
   276  		errs = packer.MultiErrorAppend(
   277  			errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', or 'none' are allowed"))
   278  	}
   279  
   280  	if _, ok := netDevice[b.config.NetDevice]; !ok {
   281  		errs = packer.MultiErrorAppend(
   282  			errs, errors.New("unrecognized network device type"))
   283  	}
   284  
   285  	if _, ok := diskInterface[b.config.DiskInterface]; !ok {
   286  		errs = packer.MultiErrorAppend(
   287  			errs, errors.New("unrecognized disk interface type"))
   288  	}
   289  
   290  	if _, ok := diskCache[b.config.DiskCache]; !ok {
   291  		errs = packer.MultiErrorAppend(
   292  			errs, errors.New("unrecognized disk cache type"))
   293  	}
   294  
   295  	if _, ok := diskDiscard[b.config.DiskDiscard]; !ok {
   296  		errs = packer.MultiErrorAppend(
   297  			errs, errors.New("unrecognized disk discard type"))
   298  	}
   299  
   300  	if _, ok := diskDZeroes[b.config.DetectZeroes]; !ok {
   301  		errs = packer.MultiErrorAppend(
   302  			errs, errors.New("unrecognized disk detect zeroes setting"))
   303  	}
   304  
   305  	if !b.config.PackerForce {
   306  		if _, err := os.Stat(b.config.OutputDir); err == nil {
   307  			errs = packer.MultiErrorAppend(
   308  				errs,
   309  				fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
   310  		}
   311  	}
   312  
   313  	if b.config.RawShutdownTimeout == "" {
   314  		b.config.RawShutdownTimeout = "5m"
   315  	}
   316  
   317  	b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
   318  	if err != nil {
   319  		errs = packer.MultiErrorAppend(
   320  			errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
   321  	}
   322  
   323  	if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
   324  		errs = packer.MultiErrorAppend(
   325  			errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
   326  	}
   327  
   328  	if b.config.VNCPortMin > b.config.VNCPortMax {
   329  		errs = packer.MultiErrorAppend(
   330  			errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
   331  	}
   332  
   333  	if b.config.QemuArgs == nil {
   334  		b.config.QemuArgs = make([][]string, 0)
   335  	}
   336  
   337  	if errs != nil && len(errs.Errors) > 0 {
   338  		return warnings, errs
   339  	}
   340  
   341  	return warnings, nil
   342  }
   343  
   344  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   345  	// Create the driver that we'll use to communicate with Qemu
   346  	driver, err := b.newDriver(b.config.QemuBinary)
   347  	if err != nil {
   348  		return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
   349  	}
   350  
   351  	steprun := &stepRun{}
   352  	if !b.config.DiskImage {
   353  		steprun.BootDrive = "once=d"
   354  		steprun.Message = "Starting VM, booting from CD-ROM"
   355  	} else {
   356  		steprun.BootDrive = "c"
   357  		steprun.Message = "Starting VM, booting disk image"
   358  	}
   359  
   360  	steps := []multistep.Step{}
   361  	if !b.config.ISOSkipCache {
   362  		steps = append(steps, &common.StepDownload{
   363  			Checksum:     b.config.ISOChecksum,
   364  			ChecksumType: b.config.ISOChecksumType,
   365  			Description:  "ISO",
   366  			Extension:    b.config.TargetExtension,
   367  			ResultKey:    "iso_path",
   368  			TargetPath:   b.config.TargetPath,
   369  			Url:          b.config.ISOUrls,
   370  		},
   371  		)
   372  	} else {
   373  		steps = append(steps, &stepSetISO{
   374  			ResultKey: "iso_path",
   375  			Url:       b.config.ISOUrls,
   376  		},
   377  		)
   378  	}
   379  
   380  	steps = append(steps, new(stepPrepareOutputDir),
   381  		&common.StepCreateFloppy{
   382  			Files:       b.config.FloppyConfig.FloppyFiles,
   383  			Directories: b.config.FloppyConfig.FloppyDirectories,
   384  		},
   385  		new(stepCreateDisk),
   386  		new(stepCopyDisk),
   387  		new(stepResizeDisk),
   388  		&common.StepHTTPServer{
   389  			HTTPDir:     b.config.HTTPDir,
   390  			HTTPPortMin: b.config.HTTPPortMin,
   391  			HTTPPortMax: b.config.HTTPPortMax,
   392  		},
   393  	)
   394  
   395  	if b.config.Comm.Type != "none" {
   396  		steps = append(steps,
   397  			new(stepForwardSSH),
   398  		)
   399  	}
   400  
   401  	steps = append(steps,
   402  		new(stepConfigureVNC),
   403  		steprun,
   404  		&stepTypeBootCommand{},
   405  	)
   406  
   407  	if b.config.Comm.Type != "none" {
   408  		steps = append(steps,
   409  			&communicator.StepConnect{
   410  				Config:    &b.config.Comm,
   411  				Host:      commHost,
   412  				SSHConfig: b.config.Comm.SSHConfigFunc(),
   413  				SSHPort:   commPort,
   414  				WinRMPort: commPort,
   415  			},
   416  		)
   417  	}
   418  
   419  	steps = append(steps,
   420  		new(common.StepProvision),
   421  	)
   422  
   423  	steps = append(steps,
   424  		&common.StepCleanupTempKeys{
   425  			Comm: &b.config.Comm,
   426  		},
   427  	)
   428  	steps = append(steps,
   429  		new(stepShutdown),
   430  	)
   431  
   432  	steps = append(steps,
   433  		new(stepConvertDisk),
   434  	)
   435  
   436  	// Setup the state bag
   437  	state := new(multistep.BasicStateBag)
   438  	state.Put("cache", cache)
   439  	state.Put("config", &b.config)
   440  	state.Put("debug", b.config.PackerDebug)
   441  	state.Put("driver", driver)
   442  	state.Put("hook", hook)
   443  	state.Put("ui", ui)
   444  
   445  	// Run
   446  	b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
   447  	b.runner.Run(state)
   448  
   449  	// If there was an error, return that
   450  	if rawErr, ok := state.GetOk("error"); ok {
   451  		return nil, rawErr.(error)
   452  	}
   453  
   454  	// If we were interrupted or cancelled, then just exit.
   455  	if _, ok := state.GetOk(multistep.StateCancelled); ok {
   456  		return nil, errors.New("Build was cancelled.")
   457  	}
   458  
   459  	if _, ok := state.GetOk(multistep.StateHalted); ok {
   460  		return nil, errors.New("Build was halted.")
   461  	}
   462  
   463  	// Compile the artifact list
   464  	files := make([]string, 0, 5)
   465  	visit := func(path string, info os.FileInfo, err error) error {
   466  		if err != nil {
   467  			return err
   468  		}
   469  		if !info.IsDir() {
   470  			files = append(files, path)
   471  		}
   472  
   473  		return nil
   474  	}
   475  
   476  	if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
   477  		return nil, err
   478  	}
   479  
   480  	artifact := &Artifact{
   481  		dir:   b.config.OutputDir,
   482  		f:     files,
   483  		state: make(map[string]interface{}),
   484  	}
   485  
   486  	artifact.state["diskName"] = state.Get("disk_filename").(string)
   487  	artifact.state["diskType"] = b.config.Format
   488  	artifact.state["diskSize"] = uint64(b.config.DiskSize)
   489  	artifact.state["domainType"] = b.config.Accelerator
   490  
   491  	return artifact, nil
   492  }
   493  
   494  func (b *Builder) Cancel() {
   495  	if b.runner != nil {
   496  		log.Println("Cancelling the step runner...")
   497  		b.runner.Cancel()
   498  	}
   499  }
   500  
   501  func (b *Builder) newDriver(qemuBinary string) (Driver, error) {
   502  	qemuPath, err := exec.LookPath(qemuBinary)
   503  	if err != nil {
   504  		return nil, err
   505  	}
   506  
   507  	qemuImgPath, err := exec.LookPath("qemu-img")
   508  	if err != nil {
   509  		return nil, err
   510  	}
   511  
   512  	log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath)
   513  	driver := &QemuDriver{
   514  		QemuPath:    qemuPath,
   515  		QemuImgPath: qemuImgPath,
   516  	}
   517  
   518  	if err := driver.Verify(); err != nil {
   519  		return nil, err
   520  	}
   521  
   522  	return driver, nil
   523  }