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