github.com/rahart/packer@v0.12.2-0.20161229105310-282bb6ad370f/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  	"runtime"
    11  	"time"
    12  
    13  	"github.com/mitchellh/multistep"
    14  	"github.com/mitchellh/packer/common"
    15  	"github.com/mitchellh/packer/helper/communicator"
    16  	"github.com/mitchellh/packer/helper/config"
    17  	"github.com/mitchellh/packer/packer"
    18  	"github.com/mitchellh/packer/template/interpolate"
    19  )
    20  
    21  const BuilderId = "transcend.qemu"
    22  
    23  var accels = map[string]struct{}{
    24  	"none": {},
    25  	"kvm":  {},
    26  	"tcg":  {},
    27  	"xen":  {},
    28  }
    29  
    30  var netDevice = map[string]bool{
    31  	"ne2k_pci":       true,
    32  	"i82551":         true,
    33  	"i82557b":        true,
    34  	"i82559er":       true,
    35  	"rtl8139":        true,
    36  	"e1000":          true,
    37  	"pcnet":          true,
    38  	"virtio":         true,
    39  	"virtio-net":     true,
    40  	"virtio-net-pci": true,
    41  	"usb-net":        true,
    42  	"i82559a":        true,
    43  	"i82559b":        true,
    44  	"i82559c":        true,
    45  	"i82550":         true,
    46  	"i82562":         true,
    47  	"i82557a":        true,
    48  	"i82557c":        true,
    49  	"i82801":         true,
    50  	"vmxnet3":        true,
    51  	"i82558a":        true,
    52  	"i82558b":        true,
    53  }
    54  
    55  var diskInterface = map[string]bool{
    56  	"ide":         true,
    57  	"scsi":        true,
    58  	"virtio":      true,
    59  	"virtio-scsi": true,
    60  }
    61  
    62  var diskCache = map[string]bool{
    63  	"writethrough": true,
    64  	"writeback":    true,
    65  	"none":         true,
    66  	"unsafe":       true,
    67  	"directsync":   true,
    68  }
    69  
    70  var diskDiscard = map[string]bool{
    71  	"unmap":  true,
    72  	"ignore": true,
    73  }
    74  
    75  type Builder struct {
    76  	config Config
    77  	runner multistep.Runner
    78  }
    79  
    80  type Config struct {
    81  	common.PackerConfig `mapstructure:",squash"`
    82  	common.HTTPConfig   `mapstructure:",squash"`
    83  	common.ISOConfig    `mapstructure:",squash"`
    84  	Comm                communicator.Config `mapstructure:",squash"`
    85  	common.FloppyConfig `mapstructure:",squash"`
    86  
    87  	ISOSkipCache      bool       `mapstructure:"iso_skip_cache"`
    88  	Accelerator       string     `mapstructure:"accelerator"`
    89  	BootCommand       []string   `mapstructure:"boot_command"`
    90  	DiskInterface     string     `mapstructure:"disk_interface"`
    91  	DiskSize          uint       `mapstructure:"disk_size"`
    92  	DiskCache         string     `mapstructure:"disk_cache"`
    93  	DiskDiscard       string     `mapstructure:"disk_discard"`
    94  	SkipCompaction    bool       `mapstructure:"skip_compaction"`
    95  	DiskCompression   bool       `mapstructure:"disk_compression"`
    96  	Format            string     `mapstructure:"format"`
    97  	Headless          bool       `mapstructure:"headless"`
    98  	DiskImage         bool       `mapstructure:"disk_image"`
    99  	MachineType       string     `mapstructure:"machine_type"`
   100  	NetDevice         string     `mapstructure:"net_device"`
   101  	OutputDir         string     `mapstructure:"output_directory"`
   102  	QemuArgs          [][]string `mapstructure:"qemuargs"`
   103  	QemuBinary        string     `mapstructure:"qemu_binary"`
   104  	ShutdownCommand   string     `mapstructure:"shutdown_command"`
   105  	SSHHostPortMin    uint       `mapstructure:"ssh_host_port_min"`
   106  	SSHHostPortMax    uint       `mapstructure:"ssh_host_port_max"`
   107  	UseDefaultDisplay bool       `mapstructure:"use_default_display"`
   108  	VNCBindAddress    string     `mapstructure:"vnc_bind_address"`
   109  	VNCPortMin        uint       `mapstructure:"vnc_port_min"`
   110  	VNCPortMax        uint       `mapstructure:"vnc_port_max"`
   111  	VMName            string     `mapstructure:"vm_name"`
   112  
   113  	// These are deprecated, but we keep them around for BC
   114  	// TODO(@mitchellh): remove
   115  	SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
   116  
   117  	// TODO(mitchellh): deprecate
   118  	RunOnce bool `mapstructure:"run_once"`
   119  
   120  	RawBootWait        string `mapstructure:"boot_wait"`
   121  	RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
   122  
   123  	bootWait        time.Duration ``
   124  	shutdownTimeout time.Duration ``
   125  	ctx             interpolate.Context
   126  }
   127  
   128  func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
   129  	err := config.Decode(&b.config, &config.DecodeOpts{
   130  		Interpolate:        true,
   131  		InterpolateContext: &b.config.ctx,
   132  		InterpolateFilter: &interpolate.RenderFilter{
   133  			Exclude: []string{
   134  				"boot_command",
   135  				"qemuargs",
   136  			},
   137  		},
   138  	}, raws...)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	var errs *packer.MultiError
   144  	warnings := make([]string, 0)
   145  
   146  	if b.config.DiskSize == 0 {
   147  		b.config.DiskSize = 40000
   148  	}
   149  
   150  	if b.config.DiskCache == "" {
   151  		b.config.DiskCache = "writeback"
   152  	}
   153  
   154  	if b.config.DiskDiscard == "" {
   155  		b.config.DiskDiscard = "ignore"
   156  	}
   157  
   158  	if b.config.Accelerator == "" {
   159  		if runtime.GOOS == "windows" {
   160  			b.config.Accelerator = "tcg"
   161  		} else {
   162  			// /dev/kvm is a kernel module that may be loaded if kvm is
   163  			// installed and the host supports VT-x extensions. To make sure
   164  			// this will actually work we need to os.Open() it. If os.Open fails
   165  			// the kernel module was not installed or loaded correctly.
   166  			if fp, err := os.Open("/dev/kvm"); err != nil {
   167  				b.config.Accelerator = "tcg"
   168  			} else {
   169  				fp.Close()
   170  				b.config.Accelerator = "kvm"
   171  			}
   172  		}
   173  		log.Printf("use detected accelerator: %s", b.config.Accelerator)
   174  	} else {
   175  		log.Printf("use specified accelerator: %s", b.config.Accelerator)
   176  	}
   177  
   178  	if b.config.MachineType == "" {
   179  		b.config.MachineType = "pc"
   180  	}
   181  
   182  	if b.config.OutputDir == "" {
   183  		b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
   184  	}
   185  
   186  	if b.config.QemuBinary == "" {
   187  		b.config.QemuBinary = "qemu-system-x86_64"
   188  	}
   189  
   190  	if b.config.RawBootWait == "" {
   191  		b.config.RawBootWait = "10s"
   192  	}
   193  
   194  	if b.config.SSHHostPortMin == 0 {
   195  		b.config.SSHHostPortMin = 2222
   196  	}
   197  
   198  	if b.config.SSHHostPortMax == 0 {
   199  		b.config.SSHHostPortMax = 4444
   200  	}
   201  
   202  	if b.config.VNCBindAddress == "" {
   203  		b.config.VNCBindAddress = "127.0.0.1"
   204  	}
   205  
   206  	if b.config.VNCPortMin == 0 {
   207  		b.config.VNCPortMin = 5900
   208  	}
   209  
   210  	if b.config.VNCPortMax == 0 {
   211  		b.config.VNCPortMax = 6000
   212  	}
   213  
   214  	if b.config.VMName == "" {
   215  		b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
   216  	}
   217  
   218  	if b.config.Format == "" {
   219  		b.config.Format = "qcow2"
   220  	}
   221  
   222  	errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
   223  
   224  	if b.config.NetDevice == "" {
   225  		b.config.NetDevice = "virtio-net"
   226  	}
   227  
   228  	if b.config.DiskInterface == "" {
   229  		b.config.DiskInterface = "virtio"
   230  	}
   231  
   232  	// TODO: backwards compatibility, write fixer instead
   233  	if b.config.SSHWaitTimeout != 0 {
   234  		b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout
   235  	}
   236  
   237  	if b.config.ISOSkipCache {
   238  		b.config.ISOChecksumType = "none"
   239  	}
   240  
   241  	isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx)
   242  	warnings = append(warnings, isoWarnings...)
   243  	errs = packer.MultiErrorAppend(errs, isoErrs...)
   244  
   245  	errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...)
   246  	if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 {
   247  		errs = packer.MultiErrorAppend(errs, es...)
   248  	}
   249  
   250  	if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
   251  		errs = packer.MultiErrorAppend(
   252  			errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
   253  	}
   254  
   255  	if b.config.Format != "qcow2" {
   256  		b.config.SkipCompaction = true
   257  		b.config.DiskCompression = false
   258  	}
   259  
   260  	if _, ok := accels[b.config.Accelerator]; !ok {
   261  		errs = packer.MultiErrorAppend(
   262  			errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', or 'none' are allowed"))
   263  	}
   264  
   265  	if _, ok := netDevice[b.config.NetDevice]; !ok {
   266  		errs = packer.MultiErrorAppend(
   267  			errs, errors.New("unrecognized network device type"))
   268  	}
   269  
   270  	if _, ok := diskInterface[b.config.DiskInterface]; !ok {
   271  		errs = packer.MultiErrorAppend(
   272  			errs, errors.New("unrecognized disk interface type"))
   273  	}
   274  
   275  	if _, ok := diskCache[b.config.DiskCache]; !ok {
   276  		errs = packer.MultiErrorAppend(
   277  			errs, errors.New("unrecognized disk cache type"))
   278  	}
   279  
   280  	if _, ok := diskDiscard[b.config.DiskDiscard]; !ok {
   281  		errs = packer.MultiErrorAppend(
   282  			errs, errors.New("unrecognized disk cache type"))
   283  	}
   284  
   285  	if !b.config.PackerForce {
   286  		if _, err := os.Stat(b.config.OutputDir); err == nil {
   287  			errs = packer.MultiErrorAppend(
   288  				errs,
   289  				fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
   290  		}
   291  	}
   292  
   293  	b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
   294  	if err != nil {
   295  		errs = packer.MultiErrorAppend(
   296  			errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
   297  	}
   298  
   299  	if b.config.RawShutdownTimeout == "" {
   300  		b.config.RawShutdownTimeout = "5m"
   301  	}
   302  
   303  	b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
   304  	if err != nil {
   305  		errs = packer.MultiErrorAppend(
   306  			errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
   307  	}
   308  
   309  	if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
   310  		errs = packer.MultiErrorAppend(
   311  			errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
   312  	}
   313  
   314  	if b.config.VNCPortMin > b.config.VNCPortMax {
   315  		errs = packer.MultiErrorAppend(
   316  			errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
   317  	}
   318  
   319  	if b.config.QemuArgs == nil {
   320  		b.config.QemuArgs = make([][]string, 0)
   321  	}
   322  
   323  	if errs != nil && len(errs.Errors) > 0 {
   324  		return warnings, errs
   325  	}
   326  
   327  	return warnings, nil
   328  }
   329  
   330  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   331  	// Create the driver that we'll use to communicate with Qemu
   332  	driver, err := b.newDriver(b.config.QemuBinary)
   333  	if err != nil {
   334  		return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
   335  	}
   336  
   337  	steprun := &stepRun{}
   338  	if !b.config.DiskImage {
   339  		steprun.BootDrive = "once=d"
   340  		steprun.Message = "Starting VM, booting from CD-ROM"
   341  	} else {
   342  		steprun.BootDrive = "c"
   343  		steprun.Message = "Starting VM, booting disk image"
   344  	}
   345  
   346  	steps := []multistep.Step{}
   347  	if !b.config.ISOSkipCache {
   348  		steps = append(steps, &common.StepDownload{
   349  			Checksum:     b.config.ISOChecksum,
   350  			ChecksumType: b.config.ISOChecksumType,
   351  			Description:  "ISO",
   352  			Extension:    "iso",
   353  			ResultKey:    "iso_path",
   354  			TargetPath:   b.config.TargetPath,
   355  			Url:          b.config.ISOUrls,
   356  		},
   357  		)
   358  	} else {
   359  		steps = append(steps, &stepSetISO{
   360  			ResultKey: "iso_path",
   361  			Url:       b.config.ISOUrls,
   362  		},
   363  		)
   364  	}
   365  
   366  	steps = append(steps, new(stepPrepareOutputDir),
   367  		&common.StepCreateFloppy{
   368  			Files:       b.config.FloppyConfig.FloppyFiles,
   369  			Directories: b.config.FloppyConfig.FloppyDirectories,
   370  		},
   371  		new(stepCreateDisk),
   372  		new(stepCopyDisk),
   373  		new(stepResizeDisk),
   374  		&common.StepHTTPServer{
   375  			HTTPDir:     b.config.HTTPDir,
   376  			HTTPPortMin: b.config.HTTPPortMin,
   377  			HTTPPortMax: b.config.HTTPPortMax,
   378  		},
   379  	)
   380  
   381  	if b.config.Comm.Type != "none" {
   382  		steps = append(steps,
   383  			new(stepForwardSSH),
   384  		)
   385  	}
   386  
   387  	steps = append(steps,
   388  		new(stepConfigureVNC),
   389  		steprun,
   390  		&stepBootWait{},
   391  		&stepTypeBootCommand{},
   392  	)
   393  
   394  	if b.config.Comm.Type != "none" {
   395  		steps = append(steps,
   396  			&communicator.StepConnect{
   397  				Config:    &b.config.Comm,
   398  				Host:      commHost,
   399  				SSHConfig: sshConfig,
   400  				SSHPort:   commPort,
   401  				WinRMPort: commPort,
   402  			},
   403  		)
   404  	}
   405  
   406  	steps = append(steps,
   407  		new(common.StepProvision),
   408  	)
   409  	steps = append(steps,
   410  		new(stepShutdown),
   411  	)
   412  
   413  	steps = append(steps,
   414  		new(stepConvertDisk),
   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("debug", b.config.PackerDebug)
   422  	state.Put("driver", driver)
   423  	state.Put("hook", hook)
   424  	state.Put("ui", ui)
   425  
   426  	// Run
   427  	b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
   428  	b.runner.Run(state)
   429  
   430  	// If there was an error, return that
   431  	if rawErr, ok := state.GetOk("error"); ok {
   432  		return nil, rawErr.(error)
   433  	}
   434  
   435  	// If we were interrupted or cancelled, then just exit.
   436  	if _, ok := state.GetOk(multistep.StateCancelled); ok {
   437  		return nil, errors.New("Build was cancelled.")
   438  	}
   439  
   440  	if _, ok := state.GetOk(multistep.StateHalted); ok {
   441  		return nil, errors.New("Build was halted.")
   442  	}
   443  
   444  	// Compile the artifact list
   445  	files := make([]string, 0, 5)
   446  	visit := func(path string, info os.FileInfo, err error) error {
   447  		if !info.IsDir() {
   448  			files = append(files, path)
   449  		}
   450  
   451  		return err
   452  	}
   453  
   454  	if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
   455  		return nil, err
   456  	}
   457  
   458  	artifact := &Artifact{
   459  		dir:   b.config.OutputDir,
   460  		f:     files,
   461  		state: make(map[string]interface{}),
   462  	}
   463  
   464  	artifact.state["diskName"] = state.Get("disk_filename").(string)
   465  	artifact.state["diskType"] = b.config.Format
   466  	artifact.state["diskSize"] = uint64(b.config.DiskSize)
   467  	artifact.state["domainType"] = b.config.Accelerator
   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  }