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 }