github.com/sneal/packer@v0.5.2/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_password":      &b.config.SSHPassword,
   200  		"ssh_username":      &b.config.SSHUser,
   201  		"vm_name":           &b.config.VMName,
   202  		"format":            &b.config.Format,
   203  		"boot_wait":         &b.config.RawBootWait,
   204  		"shutdown_timeout":  &b.config.RawShutdownTimeout,
   205  		"ssh_wait_timeout":  &b.config.RawSSHWaitTimeout,
   206  		"accelerator":       &b.config.Accelerator,
   207  		"net_device":        &b.config.NetDevice,
   208  		"disk_interface":    &b.config.DiskInterface,
   209  	}
   210  
   211  	for n, ptr := range templates {
   212  		var err error
   213  		*ptr, err = b.config.tpl.Process(*ptr, nil)
   214  		if err != nil {
   215  			errs = packer.MultiErrorAppend(
   216  				errs, fmt.Errorf("Error processing %s: %s", n, err))
   217  		}
   218  	}
   219  
   220  	for i, url := range b.config.ISOUrls {
   221  		var err error
   222  		b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil)
   223  		if err != nil {
   224  			errs = packer.MultiErrorAppend(
   225  				errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err))
   226  		}
   227  	}
   228  
   229  	for i, command := range b.config.BootCommand {
   230  		if err := b.config.tpl.Validate(command); err != nil {
   231  			errs = packer.MultiErrorAppend(errs,
   232  				fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
   233  		}
   234  	}
   235  
   236  	for i, file := range b.config.FloppyFiles {
   237  		var err error
   238  		b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
   239  		if err != nil {
   240  			errs = packer.MultiErrorAppend(errs,
   241  				fmt.Errorf("Error processing floppy_files[%d]: %s",
   242  					i, err))
   243  		}
   244  	}
   245  
   246  	if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
   247  		errs = packer.MultiErrorAppend(
   248  			errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
   249  	}
   250  
   251  	if !(b.config.Accelerator == "kvm" || b.config.Accelerator == "xen") {
   252  		errs = packer.MultiErrorAppend(
   253  			errs, errors.New("invalid format, only 'kvm' or 'xen' are allowed"))
   254  	}
   255  
   256  	if _, ok := netDevice[b.config.NetDevice]; !ok {
   257  		errs = packer.MultiErrorAppend(
   258  			errs, errors.New("unrecognized network device type"))
   259  	}
   260  
   261  	if _, ok := diskInterface[b.config.DiskInterface]; !ok {
   262  		errs = packer.MultiErrorAppend(
   263  			errs, errors.New("unrecognized disk interface type"))
   264  	}
   265  
   266  	if b.config.HTTPPortMin > b.config.HTTPPortMax {
   267  		errs = packer.MultiErrorAppend(
   268  			errs, errors.New("http_port_min must be less than http_port_max"))
   269  	}
   270  
   271  	if b.config.ISOChecksum == "" {
   272  		errs = packer.MultiErrorAppend(
   273  			errs, errors.New("Due to large file sizes, an iso_checksum is required"))
   274  	} else {
   275  		b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
   276  	}
   277  
   278  	if b.config.ISOChecksumType == "" {
   279  		errs = packer.MultiErrorAppend(
   280  			errs, errors.New("The iso_checksum_type must be specified."))
   281  	} else {
   282  		b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
   283  		if h := common.HashForType(b.config.ISOChecksumType); h == nil {
   284  			errs = packer.MultiErrorAppend(
   285  				errs,
   286  				fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
   287  		}
   288  	}
   289  
   290  	if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
   291  		errs = packer.MultiErrorAppend(
   292  			errs, errors.New("One of iso_url or iso_urls must be specified."))
   293  	} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
   294  		errs = packer.MultiErrorAppend(
   295  			errs, errors.New("Only one of iso_url or iso_urls may be specified."))
   296  	} else if b.config.RawSingleISOUrl != "" {
   297  		b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
   298  	}
   299  
   300  	for i, url := range b.config.ISOUrls {
   301  		b.config.ISOUrls[i], err = common.DownloadableURL(url)
   302  		if err != nil {
   303  			errs = packer.MultiErrorAppend(
   304  				errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
   305  		}
   306  	}
   307  
   308  	if !b.config.PackerForce {
   309  		if _, err := os.Stat(b.config.OutputDir); err == nil {
   310  			errs = packer.MultiErrorAppend(
   311  				errs,
   312  				fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
   313  		}
   314  	}
   315  
   316  	b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
   317  	if err != nil {
   318  		errs = packer.MultiErrorAppend(
   319  			errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
   320  	}
   321  
   322  	if b.config.RawShutdownTimeout == "" {
   323  		b.config.RawShutdownTimeout = "5m"
   324  	}
   325  
   326  	if b.config.RawSSHWaitTimeout == "" {
   327  		b.config.RawSSHWaitTimeout = "20m"
   328  	}
   329  
   330  	b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
   331  	if err != nil {
   332  		errs = packer.MultiErrorAppend(
   333  			errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
   334  	}
   335  
   336  	if b.config.SSHKeyPath != "" {
   337  		if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
   338  			errs = packer.MultiErrorAppend(
   339  				errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
   340  		} else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil {
   341  			errs = packer.MultiErrorAppend(
   342  				errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
   343  		}
   344  	}
   345  
   346  	if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
   347  		errs = packer.MultiErrorAppend(
   348  			errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
   349  	}
   350  
   351  	if b.config.SSHUser == "" {
   352  		errs = packer.MultiErrorAppend(
   353  			errs, errors.New("An ssh_username must be specified."))
   354  	}
   355  
   356  	b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
   357  	if err != nil {
   358  		errs = packer.MultiErrorAppend(
   359  			errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
   360  	}
   361  
   362  	if b.config.VNCPortMin > b.config.VNCPortMax {
   363  		errs = packer.MultiErrorAppend(
   364  			errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
   365  	}
   366  
   367  	if b.config.QemuArgs == nil {
   368  		b.config.QemuArgs = make([][]string, 0)
   369  	}
   370  
   371  	if errs != nil && len(errs.Errors) > 0 {
   372  		return nil, errs
   373  	}
   374  
   375  	return nil, nil
   376  }
   377  
   378  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   379  	// Create the driver that we'll use to communicate with Qemu
   380  	driver, err := b.newDriver(b.config.QemuBinary)
   381  	if err != nil {
   382  		return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
   383  	}
   384  
   385  	steps := []multistep.Step{
   386  		&common.StepDownload{
   387  			Checksum:     b.config.ISOChecksum,
   388  			ChecksumType: b.config.ISOChecksumType,
   389  			Description:  "ISO",
   390  			ResultKey:    "iso_path",
   391  			Url:          b.config.ISOUrls,
   392  		},
   393  		new(stepPrepareOutputDir),
   394  		&common.StepCreateFloppy{
   395  			Files: b.config.FloppyFiles,
   396  		},
   397  		new(stepCreateDisk),
   398  		new(stepHTTPServer),
   399  		new(stepForwardSSH),
   400  		new(stepConfigureVNC),
   401  		&stepRun{
   402  			BootDrive: "once=d",
   403  			Message:   "Starting VM, booting from CD-ROM",
   404  		},
   405  		&stepBootWait{},
   406  		&stepTypeBootCommand{},
   407  		&common.StepConnectSSH{
   408  			SSHAddress:     sshAddress,
   409  			SSHConfig:      sshConfig,
   410  			SSHWaitTimeout: b.config.sshWaitTimeout,
   411  		},
   412  		new(common.StepProvision),
   413  		new(stepShutdown),
   414  	}
   415  
   416  	// Setup the state bag
   417  	state := new(multistep.BasicStateBag)
   418  	state.Put("cache", cache)
   419  	state.Put("config", &b.config)
   420  	state.Put("driver", driver)
   421  	state.Put("hook", hook)
   422  	state.Put("ui", ui)
   423  
   424  	// Run
   425  	if b.config.PackerDebug {
   426  		b.runner = &multistep.DebugRunner{
   427  			Steps:   steps,
   428  			PauseFn: common.MultistepDebugFn(ui),
   429  		}
   430  	} else {
   431  		b.runner = &multistep.BasicRunner{Steps: steps}
   432  	}
   433  
   434  	b.runner.Run(state)
   435  
   436  	// If there was an error, return that
   437  	if rawErr, ok := state.GetOk("error"); ok {
   438  		return nil, rawErr.(error)
   439  	}
   440  
   441  	// If we were interrupted or cancelled, then just exit.
   442  	if _, ok := state.GetOk(multistep.StateCancelled); ok {
   443  		return nil, errors.New("Build was cancelled.")
   444  	}
   445  
   446  	if _, ok := state.GetOk(multistep.StateHalted); ok {
   447  		return nil, errors.New("Build was halted.")
   448  	}
   449  
   450  	// Compile the artifact list
   451  	files := make([]string, 0, 5)
   452  	visit := func(path string, info os.FileInfo, err error) error {
   453  		if !info.IsDir() {
   454  			files = append(files, path)
   455  		}
   456  
   457  		return err
   458  	}
   459  
   460  	if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
   461  		return nil, err
   462  	}
   463  
   464  	artifact := &Artifact{
   465  		dir: b.config.OutputDir,
   466  		f:   files,
   467  	}
   468  
   469  	return artifact, nil
   470  }
   471  
   472  func (b *Builder) Cancel() {
   473  	if b.runner != nil {
   474  		log.Println("Cancelling the step runner...")
   475  		b.runner.Cancel()
   476  	}
   477  }
   478  
   479  func (b *Builder) newDriver(qemuBinary string) (Driver, error) {
   480  	qemuPath, err := exec.LookPath(qemuBinary)
   481  	if err != nil {
   482  		return nil, err
   483  	}
   484  
   485  	qemuImgPath, err := exec.LookPath("qemu-img")
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  
   490  	log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath)
   491  	driver := &QemuDriver{
   492  		QemuPath:    qemuPath,
   493  		QemuImgPath: qemuImgPath,
   494  	}
   495  
   496  	if err := driver.Verify(); err != nil {
   497  		return nil, err
   498  	}
   499  
   500  	return driver, nil
   501  }