github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/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  	"strings"
    11  	"time"
    12  
    13  	"github.com/mitchellh/multistep"
    14  	"github.com/mitchellh/packer/common"
    15  	commonssh "github.com/mitchellh/packer/common/ssh"
    16  	"github.com/mitchellh/packer/packer"
    17  )
    18  
    19  const BuilderId = "transcend.qemu"
    20  
    21  var accels = map[string]struct{}{
    22  	"none": struct{}{},
    23  	"kvm":  struct{}{},
    24  	"tcg":  struct{}{},
    25  	"xen":  struct{}{},
    26  }
    27  
    28  var netDevice = map[string]bool{
    29  	"ne2k_pci":       true,
    30  	"i82551":         true,
    31  	"i82557b":        true,
    32  	"i82559er":       true,
    33  	"rtl8139":        true,
    34  	"e1000":          true,
    35  	"pcnet":          true,
    36  	"virtio":         true,
    37  	"virtio-net":     true,
    38  	"virtio-net-pci": true,
    39  	"usb-net":        true,
    40  	"i82559a":        true,
    41  	"i82559b":        true,
    42  	"i82559c":        true,
    43  	"i82550":         true,
    44  	"i82562":         true,
    45  	"i82557a":        true,
    46  	"i82557c":        true,
    47  	"i82801":         true,
    48  	"vmxnet3":        true,
    49  	"i82558a":        true,
    50  	"i82558b":        true,
    51  }
    52  
    53  var diskInterface = map[string]bool{
    54  	"ide":    true,
    55  	"scsi":   true,
    56  	"virtio": true,
    57  }
    58  
    59  var diskCache = map[string]bool{
    60  	"writethrough": true,
    61  	"writeback":    true,
    62  	"none":         true,
    63  	"unsafe":       true,
    64  	"directsync":   true,
    65  }
    66  
    67  type Builder struct {
    68  	config config
    69  	runner multistep.Runner
    70  }
    71  
    72  type config struct {
    73  	common.PackerConfig `mapstructure:",squash"`
    74  
    75  	Accelerator     string     `mapstructure:"accelerator"`
    76  	BootCommand     []string   `mapstructure:"boot_command"`
    77  	DiskInterface   string     `mapstructure:"disk_interface"`
    78  	DiskSize        uint       `mapstructure:"disk_size"`
    79  	DiskCache       string     `mapstructure:"disk_cache"`
    80  	FloppyFiles     []string   `mapstructure:"floppy_files"`
    81  	Format          string     `mapstructure:"format"`
    82  	Headless        bool       `mapstructure:"headless"`
    83  	DiskImage       bool       `mapstructure:"disk_image"`
    84  	HTTPDir         string     `mapstructure:"http_directory"`
    85  	HTTPPortMin     uint       `mapstructure:"http_port_min"`
    86  	HTTPPortMax     uint       `mapstructure:"http_port_max"`
    87  	ISOChecksum     string     `mapstructure:"iso_checksum"`
    88  	ISOChecksumType string     `mapstructure:"iso_checksum_type"`
    89  	ISOUrls         []string   `mapstructure:"iso_urls"`
    90  	MachineType     string     `mapstructure:"machine_type"`
    91  	NetDevice       string     `mapstructure:"net_device"`
    92  	OutputDir       string     `mapstructure:"output_directory"`
    93  	QemuArgs        [][]string `mapstructure:"qemuargs"`
    94  	QemuBinary      string     `mapstructure:"qemu_binary"`
    95  	ShutdownCommand string     `mapstructure:"shutdown_command"`
    96  	SSHHostPortMin  uint       `mapstructure:"ssh_host_port_min"`
    97  	SSHHostPortMax  uint       `mapstructure:"ssh_host_port_max"`
    98  	SSHPassword     string     `mapstructure:"ssh_password"`
    99  	SSHPort         uint       `mapstructure:"ssh_port"`
   100  	SSHUser         string     `mapstructure:"ssh_username"`
   101  	SSHKeyPath      string     `mapstructure:"ssh_key_path"`
   102  	VNCPortMin      uint       `mapstructure:"vnc_port_min"`
   103  	VNCPortMax      uint       `mapstructure:"vnc_port_max"`
   104  	VMName          string     `mapstructure:"vm_name"`
   105  
   106  	// TODO(mitchellh): deprecate
   107  	RunOnce bool `mapstructure:"run_once"`
   108  
   109  	RawBootWait        string `mapstructure:"boot_wait"`
   110  	RawSingleISOUrl    string `mapstructure:"iso_url"`
   111  	RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
   112  	RawSSHWaitTimeout  string `mapstructure:"ssh_wait_timeout"`
   113  
   114  	bootWait        time.Duration ``
   115  	shutdownTimeout time.Duration ``
   116  	sshWaitTimeout  time.Duration ``
   117  	tpl             *packer.ConfigTemplate
   118  }
   119  
   120  func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
   121  	md, err := common.DecodeConfig(&b.config, raws...)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	warnings := make([]string, 0)
   126  
   127  	b.config.tpl, err = packer.NewConfigTemplate()
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	b.config.tpl.UserVars = b.config.PackerUserVars
   132  
   133  	// Accumulate any errors
   134  	errs := common.CheckUnusedConfig(md)
   135  
   136  	if b.config.DiskSize == 0 {
   137  		b.config.DiskSize = 40000
   138  	}
   139  
   140  	if b.config.DiskCache == "" {
   141  		b.config.DiskCache = "writeback"
   142  	}
   143  
   144  	if b.config.Accelerator == "" {
   145  		b.config.Accelerator = "kvm"
   146  	}
   147  
   148  	if b.config.HTTPPortMin == 0 {
   149  		b.config.HTTPPortMin = 8000
   150  	}
   151  
   152  	if b.config.HTTPPortMax == 0 {
   153  		b.config.HTTPPortMax = 9000
   154  	}
   155  
   156  	if b.config.MachineType == "" {
   157  		b.config.MachineType = "pc"
   158  	}
   159  
   160  	if b.config.OutputDir == "" {
   161  		b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
   162  	}
   163  
   164  	if b.config.QemuBinary == "" {
   165  		b.config.QemuBinary = "qemu-system-x86_64"
   166  	}
   167  
   168  	if b.config.RawBootWait == "" {
   169  		b.config.RawBootWait = "10s"
   170  	}
   171  
   172  	if b.config.SSHHostPortMin == 0 {
   173  		b.config.SSHHostPortMin = 2222
   174  	}
   175  
   176  	if b.config.SSHHostPortMax == 0 {
   177  		b.config.SSHHostPortMax = 4444
   178  	}
   179  
   180  	if b.config.SSHPort == 0 {
   181  		b.config.SSHPort = 22
   182  	}
   183  
   184  	if b.config.VNCPortMin == 0 {
   185  		b.config.VNCPortMin = 5900
   186  	}
   187  
   188  	if b.config.VNCPortMax == 0 {
   189  		b.config.VNCPortMax = 6000
   190  	}
   191  
   192  	for i, args := range b.config.QemuArgs {
   193  		for j, arg := range args {
   194  			if err := b.config.tpl.Validate(arg); err != nil {
   195  				errs = packer.MultiErrorAppend(errs,
   196  					fmt.Errorf("Error processing qemu-system_x86-64[%d][%d]: %s", i, j, err))
   197  			}
   198  		}
   199  	}
   200  
   201  	if b.config.VMName == "" {
   202  		b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
   203  	}
   204  
   205  	if b.config.Format == "" {
   206  		b.config.Format = "qcow2"
   207  	}
   208  
   209  	if b.config.FloppyFiles == nil {
   210  		b.config.FloppyFiles = make([]string, 0)
   211  	}
   212  
   213  	if b.config.NetDevice == "" {
   214  		b.config.NetDevice = "virtio-net"
   215  	}
   216  
   217  	if b.config.DiskInterface == "" {
   218  		b.config.DiskInterface = "virtio"
   219  	}
   220  
   221  	// Errors
   222  	templates := map[string]*string{
   223  		"http_directory":    &b.config.HTTPDir,
   224  		"iso_checksum":      &b.config.ISOChecksum,
   225  		"iso_checksum_type": &b.config.ISOChecksumType,
   226  		"iso_url":           &b.config.RawSingleISOUrl,
   227  		"output_directory":  &b.config.OutputDir,
   228  		"shutdown_command":  &b.config.ShutdownCommand,
   229  		"ssh_key_path":      &b.config.SSHKeyPath,
   230  		"ssh_password":      &b.config.SSHPassword,
   231  		"ssh_username":      &b.config.SSHUser,
   232  		"vm_name":           &b.config.VMName,
   233  		"format":            &b.config.Format,
   234  		"boot_wait":         &b.config.RawBootWait,
   235  		"shutdown_timeout":  &b.config.RawShutdownTimeout,
   236  		"ssh_wait_timeout":  &b.config.RawSSHWaitTimeout,
   237  		"accelerator":       &b.config.Accelerator,
   238  		"machine_type":      &b.config.MachineType,
   239  		"net_device":        &b.config.NetDevice,
   240  		"disk_interface":    &b.config.DiskInterface,
   241  	}
   242  
   243  	for n, ptr := range templates {
   244  		var err error
   245  		*ptr, err = b.config.tpl.Process(*ptr, nil)
   246  		if err != nil {
   247  			errs = packer.MultiErrorAppend(
   248  				errs, fmt.Errorf("Error processing %s: %s", n, err))
   249  		}
   250  	}
   251  
   252  	for i, url := range b.config.ISOUrls {
   253  		var err error
   254  		b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil)
   255  		if err != nil {
   256  			errs = packer.MultiErrorAppend(
   257  				errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err))
   258  		}
   259  	}
   260  
   261  	for i, command := range b.config.BootCommand {
   262  		if err := b.config.tpl.Validate(command); err != nil {
   263  			errs = packer.MultiErrorAppend(errs,
   264  				fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
   265  		}
   266  	}
   267  
   268  	for i, file := range b.config.FloppyFiles {
   269  		var err error
   270  		b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
   271  		if err != nil {
   272  			errs = packer.MultiErrorAppend(errs,
   273  				fmt.Errorf("Error processing floppy_files[%d]: %s",
   274  					i, err))
   275  		}
   276  	}
   277  
   278  	if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
   279  		errs = packer.MultiErrorAppend(
   280  			errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
   281  	}
   282  
   283  	if _, ok := accels[b.config.Accelerator]; !ok {
   284  		errs = packer.MultiErrorAppend(
   285  			errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', or 'none' are allowed"))
   286  	}
   287  
   288  	if _, ok := netDevice[b.config.NetDevice]; !ok {
   289  		errs = packer.MultiErrorAppend(
   290  			errs, errors.New("unrecognized network device type"))
   291  	}
   292  
   293  	if _, ok := diskInterface[b.config.DiskInterface]; !ok {
   294  		errs = packer.MultiErrorAppend(
   295  			errs, errors.New("unrecognized disk interface type"))
   296  	}
   297  
   298  	if _, ok := diskCache[b.config.DiskCache]; !ok {
   299  		errs = packer.MultiErrorAppend(
   300  			errs, errors.New("unrecognized disk cache type"))
   301  	}
   302  
   303  	if b.config.HTTPPortMin > b.config.HTTPPortMax {
   304  		errs = packer.MultiErrorAppend(
   305  			errs, errors.New("http_port_min must be less than http_port_max"))
   306  	}
   307  
   308  	if b.config.ISOChecksumType == "" {
   309  		errs = packer.MultiErrorAppend(
   310  			errs, errors.New("The iso_checksum_type must be specified."))
   311  	} else {
   312  		b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
   313  		if b.config.ISOChecksumType != "none" {
   314  			if b.config.ISOChecksum == "" {
   315  				errs = packer.MultiErrorAppend(
   316  					errs, errors.New("Due to large file sizes, an iso_checksum is required"))
   317  			} else {
   318  				b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
   319  			}
   320  
   321  			if h := common.HashForType(b.config.ISOChecksumType); h == nil {
   322  				errs = packer.MultiErrorAppend(
   323  					errs,
   324  					fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
   325  			}
   326  		}
   327  	}
   328  
   329  	if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
   330  		errs = packer.MultiErrorAppend(
   331  			errs, errors.New("One of iso_url or iso_urls must be specified."))
   332  	} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
   333  		errs = packer.MultiErrorAppend(
   334  			errs, errors.New("Only one of iso_url or iso_urls may be specified."))
   335  	} else if b.config.RawSingleISOUrl != "" {
   336  		b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
   337  	}
   338  
   339  	for i, url := range b.config.ISOUrls {
   340  		b.config.ISOUrls[i], err = common.DownloadableURL(url)
   341  		if err != nil {
   342  			errs = packer.MultiErrorAppend(
   343  				errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
   344  		}
   345  	}
   346  
   347  	if !b.config.PackerForce {
   348  		if _, err := os.Stat(b.config.OutputDir); err == nil {
   349  			errs = packer.MultiErrorAppend(
   350  				errs,
   351  				fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
   352  		}
   353  	}
   354  
   355  	b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
   356  	if err != nil {
   357  		errs = packer.MultiErrorAppend(
   358  			errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
   359  	}
   360  
   361  	if b.config.RawShutdownTimeout == "" {
   362  		b.config.RawShutdownTimeout = "5m"
   363  	}
   364  
   365  	if b.config.RawSSHWaitTimeout == "" {
   366  		b.config.RawSSHWaitTimeout = "20m"
   367  	}
   368  
   369  	b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
   370  	if err != nil {
   371  		errs = packer.MultiErrorAppend(
   372  			errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
   373  	}
   374  
   375  	if b.config.SSHKeyPath != "" {
   376  		if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
   377  			errs = packer.MultiErrorAppend(
   378  				errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
   379  		} else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil {
   380  			errs = packer.MultiErrorAppend(
   381  				errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
   382  		}
   383  	}
   384  
   385  	if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
   386  		errs = packer.MultiErrorAppend(
   387  			errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
   388  	}
   389  
   390  	if b.config.SSHUser == "" {
   391  		errs = packer.MultiErrorAppend(
   392  			errs, errors.New("An ssh_username must be specified."))
   393  	}
   394  
   395  	b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
   396  	if err != nil {
   397  		errs = packer.MultiErrorAppend(
   398  			errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
   399  	}
   400  
   401  	if b.config.VNCPortMin > b.config.VNCPortMax {
   402  		errs = packer.MultiErrorAppend(
   403  			errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
   404  	}
   405  
   406  	if b.config.QemuArgs == nil {
   407  		b.config.QemuArgs = make([][]string, 0)
   408  	}
   409  
   410  	if b.config.ISOChecksumType == "none" {
   411  		warnings = append(warnings,
   412  			"A checksum type of 'none' was specified. Since ISO files are so big,\n"+
   413  				"a checksum is highly recommended.")
   414  	}
   415  
   416  	if errs != nil && len(errs.Errors) > 0 {
   417  		return warnings, errs
   418  	}
   419  
   420  	return warnings, nil
   421  }
   422  
   423  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   424  	// Create the driver that we'll use to communicate with Qemu
   425  	driver, err := b.newDriver(b.config.QemuBinary)
   426  	if err != nil {
   427  		return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
   428  	}
   429  
   430  	steprun := &stepRun{}
   431  	if !b.config.DiskImage {
   432  		steprun.BootDrive = "once=d"
   433  		steprun.Message = "Starting VM, booting from CD-ROM"
   434  	} else {
   435  		steprun.BootDrive = "c"
   436  		steprun.Message = "Starting VM, booting disk image"
   437  	}
   438  
   439  	steps := []multistep.Step{
   440  		&common.StepDownload{
   441  			Checksum:     b.config.ISOChecksum,
   442  			ChecksumType: b.config.ISOChecksumType,
   443  			Description:  "ISO",
   444  			ResultKey:    "iso_path",
   445  			Url:          b.config.ISOUrls,
   446  		},
   447  		new(stepPrepareOutputDir),
   448  		&common.StepCreateFloppy{
   449  			Files: b.config.FloppyFiles,
   450  		},
   451  		new(stepCreateDisk),
   452  		new(stepCopyDisk),
   453  		new(stepResizeDisk),
   454  		new(stepHTTPServer),
   455  		new(stepForwardSSH),
   456  		new(stepConfigureVNC),
   457  		steprun,
   458  		&stepBootWait{},
   459  		&stepTypeBootCommand{},
   460  		&common.StepConnectSSH{
   461  			SSHAddress:     sshAddress,
   462  			SSHConfig:      sshConfig,
   463  			SSHWaitTimeout: b.config.sshWaitTimeout,
   464  		},
   465  		new(common.StepProvision),
   466  		new(stepShutdown),
   467  	}
   468  
   469  	// Setup the state bag
   470  	state := new(multistep.BasicStateBag)
   471  	state.Put("cache", cache)
   472  	state.Put("config", &b.config)
   473  	state.Put("driver", driver)
   474  	state.Put("hook", hook)
   475  	state.Put("ui", ui)
   476  
   477  	// Run
   478  	if b.config.PackerDebug {
   479  		b.runner = &multistep.DebugRunner{
   480  			Steps:   steps,
   481  			PauseFn: common.MultistepDebugFn(ui),
   482  		}
   483  	} else {
   484  		b.runner = &multistep.BasicRunner{Steps: steps}
   485  	}
   486  
   487  	b.runner.Run(state)
   488  
   489  	// If there was an error, return that
   490  	if rawErr, ok := state.GetOk("error"); ok {
   491  		return nil, rawErr.(error)
   492  	}
   493  
   494  	// If we were interrupted or cancelled, then just exit.
   495  	if _, ok := state.GetOk(multistep.StateCancelled); ok {
   496  		return nil, errors.New("Build was cancelled.")
   497  	}
   498  
   499  	if _, ok := state.GetOk(multistep.StateHalted); ok {
   500  		return nil, errors.New("Build was halted.")
   501  	}
   502  
   503  	// Compile the artifact list
   504  	files := make([]string, 0, 5)
   505  	visit := func(path string, info os.FileInfo, err error) error {
   506  		if !info.IsDir() {
   507  			files = append(files, path)
   508  		}
   509  
   510  		return err
   511  	}
   512  
   513  	if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
   514  		return nil, err
   515  	}
   516  
   517  	artifact := &Artifact{
   518  		dir:   b.config.OutputDir,
   519  		f:     files,
   520  		state: make(map[string]interface{}),
   521  	}
   522  
   523  	artifact.state["diskName"] = state.Get("disk_filename").(string)
   524  	artifact.state["diskType"] = b.config.Format
   525  	artifact.state["diskSize"] = uint64(b.config.DiskSize)
   526  	artifact.state["domainType"] = b.config.Accelerator
   527  
   528  	return artifact, nil
   529  }
   530  
   531  func (b *Builder) Cancel() {
   532  	if b.runner != nil {
   533  		log.Println("Cancelling the step runner...")
   534  		b.runner.Cancel()
   535  	}
   536  }
   537  
   538  func (b *Builder) newDriver(qemuBinary string) (Driver, error) {
   539  	qemuPath, err := exec.LookPath(qemuBinary)
   540  	if err != nil {
   541  		return nil, err
   542  	}
   543  
   544  	qemuImgPath, err := exec.LookPath("qemu-img")
   545  	if err != nil {
   546  		return nil, err
   547  	}
   548  
   549  	log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath)
   550  	driver := &QemuDriver{
   551  		QemuPath:    qemuPath,
   552  		QemuImgPath: qemuImgPath,
   553  	}
   554  
   555  	if err := driver.Verify(); err != nil {
   556  		return nil, err
   557  	}
   558  
   559  	return driver, nil
   560  }