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 }