github.com/rahart/packer@v0.12.2-0.20161229105310-282bb6ad370f/builder/hyperv/iso/builder.go (about)

     1  package iso
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"strings"
     9  
    10  	"github.com/mitchellh/multistep"
    11  	hypervcommon "github.com/mitchellh/packer/builder/hyperv/common"
    12  	"github.com/mitchellh/packer/common"
    13  	powershell "github.com/mitchellh/packer/common/powershell"
    14  	"github.com/mitchellh/packer/common/powershell/hyperv"
    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 (
    22  	DefaultDiskSize = 40 * 1024        // ~40GB
    23  	MinDiskSize     = 256              // 256MB
    24  	MaxDiskSize     = 64 * 1024 * 1024 // 64TB
    25  
    26  	DefaultRamSize                 = 1 * 1024  // 1GB
    27  	MinRamSize                     = 32        // 32MB
    28  	MaxRamSize                     = 32 * 1024 // 32GB
    29  	MinNestedVirtualizationRamSize = 4 * 1024  // 4GB
    30  
    31  	LowRam = 256 // 256MB
    32  
    33  	DefaultUsername = ""
    34  	DefaultPassword = ""
    35  )
    36  
    37  // Builder implements packer.Builder and builds the actual Hyperv
    38  // images.
    39  type Builder struct {
    40  	config Config
    41  	runner multistep.Runner
    42  }
    43  
    44  type Config struct {
    45  	common.PackerConfig         `mapstructure:",squash"`
    46  	common.HTTPConfig           `mapstructure:",squash"`
    47  	common.ISOConfig            `mapstructure:",squash"`
    48  	hypervcommon.FloppyConfig   `mapstructure:",squash"`
    49  	hypervcommon.OutputConfig   `mapstructure:",squash"`
    50  	hypervcommon.SSHConfig      `mapstructure:",squash"`
    51  	hypervcommon.RunConfig      `mapstructure:",squash"`
    52  	hypervcommon.ShutdownConfig `mapstructure:",squash"`
    53  
    54  	// The size, in megabytes, of the hard disk to create for the VM.
    55  	// By default, this is 130048 (about 127 GB).
    56  	DiskSize uint `mapstructure:"disk_size"`
    57  	// The size, in megabytes, of the computer memory in the VM.
    58  	// By default, this is 1024 (about 1 GB).
    59  	RamSize uint `mapstructure:"ram_size"`
    60  	// A list of files to place onto a floppy disk that is attached when the
    61  	// VM is booted. This is most useful for unattended Windows installs,
    62  	// which look for an Autounattend.xml file on removable media. By default,
    63  	// no floppy will be attached. All files listed in this setting get
    64  	// placed into the root directory of the floppy and the floppy is attached
    65  	// as the first floppy device. Currently, no support exists for creating
    66  	// sub-directories on the floppy. Wildcard characters (*, ?, and [])
    67  	// are allowed. Directory names are also allowed, which will add all
    68  	// the files found in the directory to the floppy.
    69  	FloppyFiles []string `mapstructure:"floppy_files"`
    70  	//
    71  	SecondaryDvdImages []string `mapstructure:"secondary_iso_images"`
    72  
    73  	// Should integration services iso be mounted
    74  	GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
    75  
    76  	// The path to the integration services iso
    77  	GuestAdditionsPath string `mapstructure:"guest_additions_path"`
    78  
    79  	// This is the name of the new virtual machine.
    80  	// By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build.
    81  	VMName string `mapstructure:"vm_name"`
    82  
    83  	BootCommand                    []string `mapstructure:"boot_command"`
    84  	SwitchName                     string   `mapstructure:"switch_name"`
    85  	SwitchVlanId                   string   `mapstructure:"switch_vlan_id"`
    86  	VlanId                         string   `mapstructure:"vlan_id"`
    87  	Cpu                            uint     `mapstructure:"cpu"`
    88  	Generation                     uint     `mapstructure:"generation"`
    89  	EnableMacSpoofing              bool     `mapstructure:"enable_mac_spoofing"`
    90  	EnableDynamicMemory            bool     `mapstructure:"enable_dynamic_memory"`
    91  	EnableSecureBoot               bool     `mapstructure:"enable_secure_boot"`
    92  	EnableVirtualizationExtensions bool     `mapstructure:"enable_virtualization_extensions"`
    93  
    94  	Communicator string `mapstructure:"communicator"`
    95  
    96  	SkipCompaction bool `mapstructure:"skip_compaction"`
    97  
    98  	ctx interpolate.Context
    99  }
   100  
   101  // Prepare processes the build configuration parameters.
   102  func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
   103  	err := config.Decode(&b.config, &config.DecodeOpts{
   104  		Interpolate: true,
   105  		InterpolateFilter: &interpolate.RenderFilter{
   106  			Exclude: []string{
   107  				"boot_command",
   108  			},
   109  		},
   110  	}, raws...)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	// Accumulate any errors and warnings
   116  	var errs *packer.MultiError
   117  	warnings := make([]string, 0)
   118  
   119  	isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx)
   120  	warnings = append(warnings, isoWarnings...)
   121  	errs = packer.MultiErrorAppend(errs, isoErrs...)
   122  
   123  	errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
   124  	errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...)
   125  	errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(&b.config.ctx)...)
   126  	errs = packer.MultiErrorAppend(errs, b.config.OutputConfig.Prepare(&b.config.ctx, &b.config.PackerConfig)...)
   127  	errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(&b.config.ctx)...)
   128  	errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(&b.config.ctx)...)
   129  
   130  	err = b.checkDiskSize()
   131  	if err != nil {
   132  		errs = packer.MultiErrorAppend(errs, err)
   133  	}
   134  
   135  	err = b.checkRamSize()
   136  	if err != nil {
   137  		errs = packer.MultiErrorAppend(errs, err)
   138  	}
   139  
   140  	if b.config.VMName == "" {
   141  		b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
   142  	}
   143  
   144  	log.Println(fmt.Sprintf("%s: %v", "VMName", b.config.VMName))
   145  
   146  	if b.config.SwitchName == "" {
   147  		b.config.SwitchName = b.detectSwitchName()
   148  	}
   149  
   150  	if b.config.Cpu < 1 {
   151  		b.config.Cpu = 1
   152  	}
   153  
   154  	if b.config.Generation != 2 {
   155  		b.config.Generation = 1
   156  	}
   157  
   158  	if b.config.Generation == 2 {
   159  		if len(b.config.FloppyFiles) > 0 {
   160  			err = errors.New("Generation 2 vms don't support floppy drives. Use ISO image instead.")
   161  			errs = packer.MultiErrorAppend(errs, err)
   162  		}
   163  	}
   164  
   165  	log.Println(fmt.Sprintf("Using switch %s", b.config.SwitchName))
   166  	log.Println(fmt.Sprintf("%s: %v", "SwitchName", b.config.SwitchName))
   167  
   168  	// Errors
   169  	if b.config.GuestAdditionsMode == "" {
   170  		if b.config.GuestAdditionsPath != "" {
   171  			b.config.GuestAdditionsMode = "attach"
   172  		} else {
   173  			b.config.GuestAdditionsPath = os.Getenv("WINDIR") + "\\system32\\vmguest.iso"
   174  
   175  			if _, err := os.Stat(b.config.GuestAdditionsPath); os.IsNotExist(err) {
   176  				if err != nil {
   177  					b.config.GuestAdditionsPath = ""
   178  					b.config.GuestAdditionsMode = "none"
   179  				} else {
   180  					b.config.GuestAdditionsMode = "attach"
   181  				}
   182  			}
   183  		}
   184  	}
   185  
   186  	if b.config.GuestAdditionsPath == "" && b.config.GuestAdditionsMode == "attach" {
   187  		b.config.GuestAdditionsPath = os.Getenv("WINDIR") + "\\system32\\vmguest.iso"
   188  
   189  		if _, err := os.Stat(b.config.GuestAdditionsPath); os.IsNotExist(err) {
   190  			if err != nil {
   191  				b.config.GuestAdditionsPath = ""
   192  			}
   193  		}
   194  	}
   195  
   196  	for _, isoPath := range b.config.SecondaryDvdImages {
   197  		if _, err := os.Stat(isoPath); os.IsNotExist(err) {
   198  			if err != nil {
   199  				errs = packer.MultiErrorAppend(
   200  					errs, fmt.Errorf("Secondary Dvd image does not exist: %s", err))
   201  			}
   202  		}
   203  	}
   204  
   205  	numberOfIsos := len(b.config.SecondaryDvdImages)
   206  
   207  	if b.config.GuestAdditionsMode == "attach" {
   208  		if _, err := os.Stat(b.config.GuestAdditionsPath); os.IsNotExist(err) {
   209  			if err != nil {
   210  				errs = packer.MultiErrorAppend(
   211  					errs, fmt.Errorf("Guest additions iso does not exist: %s", err))
   212  			}
   213  		}
   214  
   215  		numberOfIsos = numberOfIsos + 1
   216  	}
   217  
   218  	if b.config.Generation < 2 && numberOfIsos > 2 {
   219  		if b.config.GuestAdditionsMode == "attach" {
   220  			errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are only 2 ide controllers available, so we can't support guest additions and these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", ")))
   221  		} else {
   222  			errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are only 2 ide controllers available, so we can't support these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", ")))
   223  		}
   224  	} else if b.config.Generation > 1 && len(b.config.SecondaryDvdImages) > 16 {
   225  		if b.config.GuestAdditionsMode == "attach" {
   226  			errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are not enough drive letters available for scsi (limited to 16), so we can't support guest additions and these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", ")))
   227  		} else {
   228  			errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are not enough drive letters available for scsi (limited to 16), so we can't support these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", ")))
   229  		}
   230  	}
   231  
   232  	if b.config.EnableVirtualizationExtensions {
   233  		hasVirtualMachineVirtualizationExtensions, err := powershell.HasVirtualMachineVirtualizationExtensions()
   234  		if err != nil {
   235  			errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting virtual machine virtualization extensions support: %s", err))
   236  		} else {
   237  			if !hasVirtualMachineVirtualizationExtensions {
   238  				errs = packer.MultiErrorAppend(errs, fmt.Errorf("This version of Hyper-V does not support virtual machine virtualization extension. Please use Windows 10 or Windows Server 2016 or newer."))
   239  			}
   240  		}
   241  	}
   242  
   243  	// Warnings
   244  
   245  	if b.config.ShutdownCommand == "" {
   246  		warnings = append(warnings,
   247  			"A shutdown_command was not specified. Without a shutdown command, Packer\n"+
   248  				"will forcibly halt the virtual machine, which may result in data loss.")
   249  	}
   250  
   251  	warning := b.checkHostAvailableMemory()
   252  	if warning != "" {
   253  		warnings = appendWarnings(warnings, warning)
   254  	}
   255  
   256  	if b.config.EnableVirtualizationExtensions {
   257  		if b.config.EnableDynamicMemory {
   258  			warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, dynamic memory should not be allowed.")
   259  			warnings = appendWarnings(warnings, warning)
   260  		}
   261  
   262  		if !b.config.EnableMacSpoofing {
   263  			warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, mac spoofing should be allowed.")
   264  			warnings = appendWarnings(warnings, warning)
   265  		}
   266  
   267  		if b.config.RamSize < MinNestedVirtualizationRamSize {
   268  			warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, there should be 4GB or more memory set for the vm, otherwise Hyper-V may fail to start any nested VMs.")
   269  			warnings = appendWarnings(warnings, warning)
   270  		}
   271  	}
   272  
   273  	if b.config.SwitchVlanId != "" {
   274  		if b.config.SwitchVlanId != b.config.VlanId {
   275  			warning = fmt.Sprintf("Switch network adaptor vlan should match virtual machine network adaptor vlan. The switch will not be able to see traffic from the VM.")
   276  			warnings = appendWarnings(warnings, warning)
   277  		}
   278  	}
   279  
   280  	if errs != nil && len(errs.Errors) > 0 {
   281  		return warnings, errs
   282  	}
   283  
   284  	return warnings, nil
   285  }
   286  
   287  // Run executes a Packer build and returns a packer.Artifact representing
   288  // a Hyperv appliance.
   289  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   290  	// Create the driver that we'll use to communicate with Hyperv
   291  	driver, err := hypervcommon.NewHypervPS4Driver()
   292  	if err != nil {
   293  		return nil, fmt.Errorf("Failed creating Hyper-V driver: %s", err)
   294  	}
   295  
   296  	// Set up the state.
   297  	state := new(multistep.BasicStateBag)
   298  	state.Put("cache", cache)
   299  	state.Put("config", &b.config)
   300  	state.Put("debug", b.config.PackerDebug)
   301  	state.Put("driver", driver)
   302  	state.Put("hook", hook)
   303  	state.Put("ui", ui)
   304  
   305  	steps := []multistep.Step{
   306  		&hypervcommon.StepCreateTempDir{},
   307  		&hypervcommon.StepOutputDir{
   308  			Force: b.config.PackerForce,
   309  			Path:  b.config.OutputDir,
   310  		},
   311  		&common.StepDownload{
   312  			Checksum:     b.config.ISOChecksum,
   313  			ChecksumType: b.config.ISOChecksumType,
   314  			Description:  "ISO",
   315  			ResultKey:    "iso_path",
   316  			Url:          b.config.ISOUrls,
   317  			Extension:    "iso",
   318  			TargetPath:   b.config.TargetPath,
   319  		},
   320  		&common.StepCreateFloppy{
   321  			Files: b.config.FloppyFiles,
   322  		},
   323  		&common.StepHTTPServer{
   324  			HTTPDir:     b.config.HTTPDir,
   325  			HTTPPortMin: b.config.HTTPPortMin,
   326  			HTTPPortMax: b.config.HTTPPortMax,
   327  		},
   328  		&hypervcommon.StepCreateSwitch{
   329  			SwitchName: b.config.SwitchName,
   330  		},
   331  		&hypervcommon.StepCreateVM{
   332  			VMName:                         b.config.VMName,
   333  			SwitchName:                     b.config.SwitchName,
   334  			RamSize:                        b.config.RamSize,
   335  			DiskSize:                       b.config.DiskSize,
   336  			Generation:                     b.config.Generation,
   337  			Cpu:                            b.config.Cpu,
   338  			EnableMacSpoofing:              b.config.EnableMacSpoofing,
   339  			EnableDynamicMemory:            b.config.EnableDynamicMemory,
   340  			EnableSecureBoot:               b.config.EnableSecureBoot,
   341  			EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions,
   342  		},
   343  		&hypervcommon.StepEnableIntegrationService{},
   344  
   345  		&hypervcommon.StepMountDvdDrive{
   346  			Generation: b.config.Generation,
   347  		},
   348  		&hypervcommon.StepMountFloppydrive{
   349  			Generation: b.config.Generation,
   350  		},
   351  
   352  		&hypervcommon.StepMountGuestAdditions{
   353  			GuestAdditionsMode: b.config.GuestAdditionsMode,
   354  			GuestAdditionsPath: b.config.GuestAdditionsPath,
   355  			Generation:         b.config.Generation,
   356  		},
   357  
   358  		&hypervcommon.StepMountSecondaryDvdImages{
   359  			IsoPaths:   b.config.SecondaryDvdImages,
   360  			Generation: b.config.Generation,
   361  		},
   362  
   363  		&hypervcommon.StepConfigureVlan{
   364  			VlanId:       b.config.VlanId,
   365  			SwitchVlanId: b.config.SwitchVlanId,
   366  		},
   367  
   368  		&hypervcommon.StepRun{
   369  			BootWait: b.config.BootWait,
   370  		},
   371  
   372  		&hypervcommon.StepTypeBootCommand{
   373  			BootCommand: b.config.BootCommand,
   374  			SwitchName:  b.config.SwitchName,
   375  			Ctx:         b.config.ctx,
   376  		},
   377  
   378  		// configure the communicator ssh, winrm
   379  		&communicator.StepConnect{
   380  			Config:    &b.config.SSHConfig.Comm,
   381  			Host:      hypervcommon.CommHost,
   382  			SSHConfig: hypervcommon.SSHConfigFunc(&b.config.SSHConfig),
   383  		},
   384  
   385  		// provision requires communicator to be setup
   386  		&common.StepProvision{},
   387  
   388  		&hypervcommon.StepShutdown{
   389  			Command: b.config.ShutdownCommand,
   390  			Timeout: b.config.ShutdownTimeout,
   391  		},
   392  
   393  		// wait for the vm to be powered off
   394  		&hypervcommon.StepWaitForPowerOff{},
   395  
   396  		// remove the secondary dvd images
   397  		// after we power down
   398  		&hypervcommon.StepUnmountSecondaryDvdImages{},
   399  		&hypervcommon.StepUnmountGuestAdditions{},
   400  		&hypervcommon.StepUnmountDvdDrive{},
   401  		&hypervcommon.StepUnmountFloppyDrive{
   402  			Generation: b.config.Generation,
   403  		},
   404  		&hypervcommon.StepExportVm{
   405  			OutputDir:      b.config.OutputDir,
   406  			SkipCompaction: b.config.SkipCompaction,
   407  		},
   408  
   409  		// the clean up actions for each step will be executed reverse order
   410  	}
   411  
   412  	// Run the steps.
   413  	if b.config.PackerDebug {
   414  		pauseFn := common.MultistepDebugFn(ui)
   415  		state.Put("pauseFn", pauseFn)
   416  		b.runner = &multistep.DebugRunner{
   417  			Steps:   steps,
   418  			PauseFn: pauseFn,
   419  		}
   420  	} else {
   421  		b.runner = &multistep.BasicRunner{Steps: steps}
   422  	}
   423  
   424  	b.runner.Run(state)
   425  
   426  	// Report any errors.
   427  	if rawErr, ok := state.GetOk("error"); ok {
   428  		return nil, rawErr.(error)
   429  	}
   430  
   431  	// If we were interrupted or cancelled, then just exit.
   432  	if _, ok := state.GetOk(multistep.StateCancelled); ok {
   433  		return nil, errors.New("Build was cancelled.")
   434  	}
   435  
   436  	if _, ok := state.GetOk(multistep.StateHalted); ok {
   437  		return nil, errors.New("Build was halted.")
   438  	}
   439  
   440  	return hypervcommon.NewArtifact(b.config.OutputDir)
   441  }
   442  
   443  // Cancel.
   444  func (b *Builder) Cancel() {
   445  	if b.runner != nil {
   446  		log.Println("Cancelling the step runner...")
   447  		b.runner.Cancel()
   448  	}
   449  }
   450  
   451  func appendWarnings(slice []string, data ...string) []string {
   452  	m := len(slice)
   453  	n := m + len(data)
   454  	if n > cap(slice) { // if necessary, reallocate
   455  		// allocate double what's needed, for future growth.
   456  		newSlice := make([]string, (n+1)*2)
   457  		copy(newSlice, slice)
   458  		slice = newSlice
   459  	}
   460  	slice = slice[0:n]
   461  	copy(slice[m:n], data)
   462  	return slice
   463  }
   464  
   465  func (b *Builder) checkDiskSize() error {
   466  	if b.config.DiskSize == 0 {
   467  		b.config.DiskSize = DefaultDiskSize
   468  	}
   469  
   470  	log.Println(fmt.Sprintf("%s: %v", "DiskSize", b.config.DiskSize))
   471  
   472  	if b.config.DiskSize < MinDiskSize {
   473  		return fmt.Errorf("disk_size: Virtual machine requires disk space >= %v GB, but defined: %v", MinDiskSize, b.config.DiskSize/1024)
   474  	} else if b.config.DiskSize > MaxDiskSize {
   475  		return fmt.Errorf("disk_size: Virtual machine requires disk space <= %v GB, but defined: %v", MaxDiskSize, b.config.DiskSize/1024)
   476  	}
   477  
   478  	return nil
   479  }
   480  
   481  func (b *Builder) checkRamSize() error {
   482  	if b.config.RamSize == 0 {
   483  		b.config.RamSize = DefaultRamSize
   484  	}
   485  
   486  	log.Println(fmt.Sprintf("%s: %v", "RamSize", b.config.RamSize))
   487  
   488  	if b.config.RamSize < MinRamSize {
   489  		return fmt.Errorf("ram_size: Virtual machine requires memory size >= %v MB, but defined: %v", MinRamSize, b.config.RamSize)
   490  	} else if b.config.RamSize > MaxRamSize {
   491  		return fmt.Errorf("ram_size: Virtual machine requires memory size <= %v MB, but defined: %v", MaxRamSize, b.config.RamSize)
   492  	}
   493  
   494  	return nil
   495  }
   496  
   497  func (b *Builder) checkHostAvailableMemory() string {
   498  	powershellAvailable, _, _ := powershell.IsPowershellAvailable()
   499  
   500  	if powershellAvailable {
   501  		freeMB := powershell.GetHostAvailableMemory()
   502  
   503  		if (freeMB - float64(b.config.RamSize)) < LowRam {
   504  			return fmt.Sprintf("Hyper-V might fail to create a VM if there is not enough free memory in the system.")
   505  		}
   506  	}
   507  
   508  	return ""
   509  }
   510  
   511  func (b *Builder) detectSwitchName() string {
   512  	powershellAvailable, _, _ := powershell.IsPowershellAvailable()
   513  
   514  	if powershellAvailable {
   515  		// no switch name, try to get one attached to a online network adapter
   516  		onlineSwitchName, err := hyperv.GetExternalOnlineVirtualSwitch()
   517  		if onlineSwitchName != "" && err == nil {
   518  			return onlineSwitchName
   519  		}
   520  	}
   521  
   522  	return fmt.Sprintf("packer-%s", b.config.PackerBuildName)
   523  }