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