github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/builder/hyperv/iso/builder.go (about)

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