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