github.com/emate/packer@v0.8.1-0.20150625195101-fe0fde195dc6/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  	"strings"
    12  	"time"
    13  
    14  	"github.com/mitchellh/multistep"
    15  	"github.com/mitchellh/packer/common"
    16  	"github.com/mitchellh/packer/helper/communicator"
    17  	"github.com/mitchellh/packer/helper/config"
    18  	"github.com/mitchellh/packer/packer"
    19  	"github.com/mitchellh/packer/template/interpolate"
    20  )
    21  
    22  const BuilderId = "transcend.qemu"
    23  
    24  var accels = map[string]struct{}{
    25  	"none": struct{}{},
    26  	"kvm":  struct{}{},
    27  	"tcg":  struct{}{},
    28  	"xen":  struct{}{},
    29  }
    30  
    31  var netDevice = map[string]bool{
    32  	"ne2k_pci":       true,
    33  	"i82551":         true,
    34  	"i82557b":        true,
    35  	"i82559er":       true,
    36  	"rtl8139":        true,
    37  	"e1000":          true,
    38  	"pcnet":          true,
    39  	"virtio":         true,
    40  	"virtio-net":     true,
    41  	"virtio-net-pci": true,
    42  	"usb-net":        true,
    43  	"i82559a":        true,
    44  	"i82559b":        true,
    45  	"i82559c":        true,
    46  	"i82550":         true,
    47  	"i82562":         true,
    48  	"i82557a":        true,
    49  	"i82557c":        true,
    50  	"i82801":         true,
    51  	"vmxnet3":        true,
    52  	"i82558a":        true,
    53  	"i82558b":        true,
    54  }
    55  
    56  var diskInterface = map[string]bool{
    57  	"ide":    true,
    58  	"scsi":   true,
    59  	"virtio": 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  	Comm                communicator.Config `mapstructure:",squash"`
    83  
    84  	Accelerator     string     `mapstructure:"accelerator"`
    85  	BootCommand     []string   `mapstructure:"boot_command"`
    86  	DiskInterface   string     `mapstructure:"disk_interface"`
    87  	DiskSize        uint       `mapstructure:"disk_size"`
    88  	DiskCache       string     `mapstructure:"disk_cache"`
    89  	DiskDiscard     string     `mapstructure:"disk_discard"`
    90  	FloppyFiles     []string   `mapstructure:"floppy_files"`
    91  	Format          string     `mapstructure:"format"`
    92  	Headless        bool       `mapstructure:"headless"`
    93  	DiskImage       bool       `mapstructure:"disk_image"`
    94  	HTTPDir         string     `mapstructure:"http_directory"`
    95  	HTTPPortMin     uint       `mapstructure:"http_port_min"`
    96  	HTTPPortMax     uint       `mapstructure:"http_port_max"`
    97  	ISOChecksum     string     `mapstructure:"iso_checksum"`
    98  	ISOChecksumType string     `mapstructure:"iso_checksum_type"`
    99  	ISOUrls         []string   `mapstructure:"iso_urls"`
   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  	RawSingleISOUrl    string `mapstructure:"iso_url"`
   122  	RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
   123  
   124  	bootWait        time.Duration ``
   125  	shutdownTimeout time.Duration ``
   126  	ctx             interpolate.Context
   127  }
   128  
   129  func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
   130  	err := config.Decode(&b.config, &config.DecodeOpts{
   131  		Interpolate:        true,
   132  		InterpolateContext: &b.config.ctx,
   133  		InterpolateFilter: &interpolate.RenderFilter{
   134  			Exclude: []string{
   135  				"boot_command",
   136  				"qemuargs",
   137  			},
   138  		},
   139  	}, raws...)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	if b.config.DiskSize == 0 {
   145  		b.config.DiskSize = 40000
   146  	}
   147  
   148  	if b.config.DiskCache == "" {
   149  		b.config.DiskCache = "writeback"
   150  	}
   151  
   152  	if b.config.DiskDiscard == "" {
   153  		b.config.DiskDiscard = "ignore"
   154  	}
   155  
   156  	if b.config.Accelerator == "" {
   157  		if runtime.GOOS == "windows" {
   158  			b.config.Accelerator = "tcg"
   159  		} else {
   160  			b.config.Accelerator = "kvm"
   161  		}
   162  	}
   163  
   164  	if b.config.HTTPPortMin == 0 {
   165  		b.config.HTTPPortMin = 8000
   166  	}
   167  
   168  	if b.config.HTTPPortMax == 0 {
   169  		b.config.HTTPPortMax = 9000
   170  	}
   171  
   172  	if b.config.MachineType == "" {
   173  		b.config.MachineType = "pc"
   174  	}
   175  
   176  	if b.config.OutputDir == "" {
   177  		b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
   178  	}
   179  
   180  	if b.config.QemuBinary == "" {
   181  		b.config.QemuBinary = "qemu-system-x86_64"
   182  	}
   183  
   184  	if b.config.RawBootWait == "" {
   185  		b.config.RawBootWait = "10s"
   186  	}
   187  
   188  	if b.config.SSHHostPortMin == 0 {
   189  		b.config.SSHHostPortMin = 2222
   190  	}
   191  
   192  	if b.config.SSHHostPortMax == 0 {
   193  		b.config.SSHHostPortMax = 4444
   194  	}
   195  
   196  	if b.config.VNCPortMin == 0 {
   197  		b.config.VNCPortMin = 5900
   198  	}
   199  
   200  	if b.config.VNCPortMax == 0 {
   201  		b.config.VNCPortMax = 6000
   202  	}
   203  
   204  	if b.config.VMName == "" {
   205  		b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
   206  	}
   207  
   208  	if b.config.Format == "" {
   209  		b.config.Format = "qcow2"
   210  	}
   211  
   212  	if b.config.FloppyFiles == nil {
   213  		b.config.FloppyFiles = make([]string, 0)
   214  	}
   215  
   216  	if b.config.NetDevice == "" {
   217  		b.config.NetDevice = "virtio-net"
   218  	}
   219  
   220  	if b.config.DiskInterface == "" {
   221  		b.config.DiskInterface = "virtio"
   222  	}
   223  
   224  	// TODO: backwards compatibility, write fixer instead
   225  	if b.config.SSHKeyPath != "" {
   226  		b.config.Comm.SSHPrivateKey = b.config.SSHKeyPath
   227  	}
   228  	if b.config.SSHWaitTimeout != 0 {
   229  		b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
   230  	}
   231  
   232  	var errs *packer.MultiError
   233  	warnings := make([]string, 0)
   234  
   235  	if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
   236  		errs = packer.MultiErrorAppend(errs, es...)
   237  	}
   238  
   239  	if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
   240  		errs = packer.MultiErrorAppend(
   241  			errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
   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.HTTPPortMin > b.config.HTTPPortMax {
   270  		errs = packer.MultiErrorAppend(
   271  			errs, errors.New("http_port_min must be less than http_port_max"))
   272  	}
   273  
   274  	if b.config.ISOChecksumType == "" {
   275  		errs = packer.MultiErrorAppend(
   276  			errs, errors.New("The iso_checksum_type must be specified."))
   277  	} else {
   278  		b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
   279  		if b.config.ISOChecksumType != "none" {
   280  			if b.config.ISOChecksum == "" {
   281  				errs = packer.MultiErrorAppend(
   282  					errs, errors.New("Due to large file sizes, an iso_checksum is required"))
   283  			} else {
   284  				b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
   285  			}
   286  
   287  			if h := common.HashForType(b.config.ISOChecksumType); h == nil {
   288  				errs = packer.MultiErrorAppend(
   289  					errs,
   290  					fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
   291  			}
   292  		}
   293  	}
   294  
   295  	if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
   296  		errs = packer.MultiErrorAppend(
   297  			errs, errors.New("One of iso_url or iso_urls must be specified."))
   298  	} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
   299  		errs = packer.MultiErrorAppend(
   300  			errs, errors.New("Only one of iso_url or iso_urls may be specified."))
   301  	} else if b.config.RawSingleISOUrl != "" {
   302  		b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
   303  	}
   304  
   305  	for i, url := range b.config.ISOUrls {
   306  		b.config.ISOUrls[i], err = common.DownloadableURL(url)
   307  		if err != nil {
   308  			errs = packer.MultiErrorAppend(
   309  				errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
   310  		}
   311  	}
   312  
   313  	if !b.config.PackerForce {
   314  		if _, err := os.Stat(b.config.OutputDir); err == nil {
   315  			errs = packer.MultiErrorAppend(
   316  				errs,
   317  				fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
   318  		}
   319  	}
   320  
   321  	b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
   322  	if err != nil {
   323  		errs = packer.MultiErrorAppend(
   324  			errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
   325  	}
   326  
   327  	if b.config.RawShutdownTimeout == "" {
   328  		b.config.RawShutdownTimeout = "5m"
   329  	}
   330  
   331  	b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
   332  	if err != nil {
   333  		errs = packer.MultiErrorAppend(
   334  			errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
   335  	}
   336  
   337  	if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
   338  		errs = packer.MultiErrorAppend(
   339  			errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
   340  	}
   341  
   342  	if b.config.VNCPortMin > b.config.VNCPortMax {
   343  		errs = packer.MultiErrorAppend(
   344  			errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
   345  	}
   346  
   347  	if b.config.QemuArgs == nil {
   348  		b.config.QemuArgs = make([][]string, 0)
   349  	}
   350  
   351  	if b.config.ISOChecksumType == "none" {
   352  		warnings = append(warnings,
   353  			"A checksum type of 'none' was specified. Since ISO files are so big,\n"+
   354  				"a checksum is highly recommended.")
   355  	}
   356  
   357  	if errs != nil && len(errs.Errors) > 0 {
   358  		return warnings, errs
   359  	}
   360  
   361  	return warnings, nil
   362  }
   363  
   364  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   365  	// Create the driver that we'll use to communicate with Qemu
   366  	driver, err := b.newDriver(b.config.QemuBinary)
   367  	if err != nil {
   368  		return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
   369  	}
   370  
   371  	steprun := &stepRun{}
   372  	if !b.config.DiskImage {
   373  		steprun.BootDrive = "once=d"
   374  		steprun.Message = "Starting VM, booting from CD-ROM"
   375  	} else {
   376  		steprun.BootDrive = "c"
   377  		steprun.Message = "Starting VM, booting disk image"
   378  	}
   379  
   380  	steps := []multistep.Step{
   381  		&common.StepDownload{
   382  			Checksum:     b.config.ISOChecksum,
   383  			ChecksumType: b.config.ISOChecksumType,
   384  			Description:  "ISO",
   385  			ResultKey:    "iso_path",
   386  			Url:          b.config.ISOUrls,
   387  		},
   388  		new(stepPrepareOutputDir),
   389  		&common.StepCreateFloppy{
   390  			Files: b.config.FloppyFiles,
   391  		},
   392  		new(stepCreateDisk),
   393  		new(stepCopyDisk),
   394  		new(stepResizeDisk),
   395  		new(stepHTTPServer),
   396  		new(stepForwardSSH),
   397  		new(stepConfigureVNC),
   398  		steprun,
   399  		&stepBootWait{},
   400  		&stepTypeBootCommand{},
   401  		&communicator.StepConnect{
   402  			Config:    &b.config.Comm,
   403  			Host:      commHost,
   404  			SSHConfig: sshConfig,
   405  			SSHPort:   commPort,
   406  		},
   407  		new(common.StepProvision),
   408  		new(stepShutdown),
   409  	}
   410  
   411  	// Setup the state bag
   412  	state := new(multistep.BasicStateBag)
   413  	state.Put("cache", cache)
   414  	state.Put("config", &b.config)
   415  	state.Put("driver", driver)
   416  	state.Put("hook", hook)
   417  	state.Put("ui", ui)
   418  
   419  	// Run
   420  	if b.config.PackerDebug {
   421  		b.runner = &multistep.DebugRunner{
   422  			Steps:   steps,
   423  			PauseFn: common.MultistepDebugFn(ui),
   424  		}
   425  	} else {
   426  		b.runner = &multistep.BasicRunner{Steps: steps}
   427  	}
   428  
   429  	b.runner.Run(state)
   430  
   431  	// If there was an error, return that
   432  	if rawErr, ok := state.GetOk("error"); ok {
   433  		return nil, rawErr.(error)
   434  	}
   435  
   436  	// If we were interrupted or cancelled, then just exit.
   437  	if _, ok := state.GetOk(multistep.StateCancelled); ok {
   438  		return nil, errors.New("Build was cancelled.")
   439  	}
   440  
   441  	if _, ok := state.GetOk(multistep.StateHalted); ok {
   442  		return nil, errors.New("Build was halted.")
   443  	}
   444  
   445  	// Compile the artifact list
   446  	files := make([]string, 0, 5)
   447  	visit := func(path string, info os.FileInfo, err error) error {
   448  		if !info.IsDir() {
   449  			files = append(files, path)
   450  		}
   451  
   452  		return err
   453  	}
   454  
   455  	if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
   456  		return nil, err
   457  	}
   458  
   459  	artifact := &Artifact{
   460  		dir:   b.config.OutputDir,
   461  		f:     files,
   462  		state: make(map[string]interface{}),
   463  	}
   464  
   465  	artifact.state["diskName"] = state.Get("disk_filename").(string)
   466  	artifact.state["diskType"] = b.config.Format
   467  	artifact.state["diskSize"] = uint64(b.config.DiskSize)
   468  	artifact.state["domainType"] = b.config.Accelerator
   469  
   470  	return artifact, nil
   471  }
   472  
   473  func (b *Builder) Cancel() {
   474  	if b.runner != nil {
   475  		log.Println("Cancelling the step runner...")
   476  		b.runner.Cancel()
   477  	}
   478  }
   479  
   480  func (b *Builder) newDriver(qemuBinary string) (Driver, error) {
   481  	qemuPath, err := exec.LookPath(qemuBinary)
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  
   486  	qemuImgPath, err := exec.LookPath("qemu-img")
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath)
   492  	driver := &QemuDriver{
   493  		QemuPath:    qemuPath,
   494  		QemuImgPath: qemuImgPath,
   495  	}
   496  
   497  	if err := driver.Verify(); err != nil {
   498  		return nil, err
   499  	}
   500  
   501  	return driver, nil
   502  }