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