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