github.com/jbronn/packer@v0.1.6-0.20140120165540-8a1364dbd817/builder/qemu/builder.go (about)

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