github.phpd.cn/hashicorp/packer@v1.3.2/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 CloneFromVMCXPath string `mapstructure:"clone_from_vmcx_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 `mapstructure:"generation"` 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 TempPath string `mapstructure:"temp_path"` 95 96 Communicator string `mapstructure:"communicator"` 97 98 SkipCompaction bool `mapstructure:"skip_compaction"` 99 100 SkipExport bool `mapstructure:"skip_export"` 101 102 Headless bool `mapstructure:"headless"` 103 104 ctx interpolate.Context 105 } 106 107 // Prepare processes the build configuration parameters. 108 func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 109 err := config.Decode(&b.config, &config.DecodeOpts{ 110 Interpolate: true, 111 InterpolateContext: &b.config.ctx, 112 InterpolateFilter: &interpolate.RenderFilter{ 113 Exclude: []string{ 114 "boot_command", 115 }, 116 }, 117 }, raws...) 118 if err != nil { 119 return nil, err 120 } 121 122 // Accumulate any errors and warnings 123 var errs *packer.MultiError 124 warnings := make([]string, 0) 125 126 if b.config.RawSingleISOUrl != "" || len(b.config.ISOUrls) > 0 { 127 isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx) 128 warnings = append(warnings, isoWarnings...) 129 errs = packer.MultiErrorAppend(errs, isoErrs...) 130 } 131 132 errs = packer.MultiErrorAppend(errs, b.config.BootConfig.Prepare(&b.config.ctx)...) 133 errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...) 134 errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...) 135 errs = packer.MultiErrorAppend(errs, b.config.OutputConfig.Prepare(&b.config.ctx, &b.config.PackerConfig)...) 136 errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(&b.config.ctx)...) 137 errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(&b.config.ctx)...) 138 139 err = b.checkRamSize() 140 if err != nil { 141 errs = packer.MultiErrorAppend(errs, err) 142 } 143 144 if b.config.VMName == "" { 145 b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) 146 } 147 148 log.Println(fmt.Sprintf("%s: %v", "VMName", b.config.VMName)) 149 150 if b.config.SwitchName == "" { 151 b.config.SwitchName = b.detectSwitchName() 152 } 153 154 if b.config.Cpu < 1 { 155 b.config.Cpu = 1 156 } 157 158 if b.config.CloneFromVMName == "" { 159 if b.config.CloneFromVMCXPath == "" { 160 errs = packer.MultiErrorAppend(errs, fmt.Errorf("The clone_from_vm_name must be specified if "+ 161 "clone_from_vmcx_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 "+ 167 "from exists: %s", err)) 168 } else { 169 if !virtualMachineExists { 170 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Virtual machine '%s' to clone from does not "+ 171 "exist.", b.config.CloneFromVMName)) 172 } else { 173 b.config.Generation, err = powershell.GetVirtualMachineGeneration(b.config.CloneFromVMName) 174 if err != nil { 175 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting virtual machine to clone "+ 176 "from generation: %s", err)) 177 } 178 179 if b.config.CloneFromSnapshotName != "" { 180 virtualMachineSnapshotExists, err := powershell.DoesVirtualMachineSnapshotExist( 181 b.config.CloneFromVMName, b.config.CloneFromSnapshotName) 182 if err != nil { 183 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting if virtual machine "+ 184 "snapshot to clone from exists: %s", err)) 185 } else { 186 if !virtualMachineSnapshotExists { 187 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Virtual machine snapshot '%s' on "+ 188 "virtual machine '%s' to clone from does not exist.", 189 b.config.CloneFromSnapshotName, b.config.CloneFromVMName)) 190 } 191 } 192 } 193 194 virtualMachineOn, err := powershell.IsVirtualMachineOn(b.config.CloneFromVMName) 195 if err != nil { 196 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting if virtual machine to "+ 197 "clone is running: %s", err)) 198 } else { 199 if virtualMachineOn { 200 warning := fmt.Sprintf("Cloning from a virtual machine that is running.") 201 warnings = appendWarnings(warnings, warning) 202 } 203 } 204 } 205 } 206 } 207 208 if b.config.CloneFromVMCXPath == "" { 209 if b.config.CloneFromVMName == "" { 210 errs = packer.MultiErrorAppend(errs, fmt.Errorf("The clone_from_vmcx_path be specified if "+ 211 "clone_from_vm_name must is not specified.")) 212 } 213 } else { 214 if _, err := os.Stat(b.config.CloneFromVMCXPath); os.IsNotExist(err) { 215 if err != nil { 216 errs = packer.MultiErrorAppend( 217 errs, fmt.Errorf("CloneFromVMCXPath does not exist: %s", err)) 218 } 219 } 220 } 221 222 if b.config.Generation < 1 || b.config.Generation > 2 { 223 b.config.Generation = 1 224 } 225 226 if b.config.Generation == 2 { 227 if len(b.config.FloppyFiles) > 0 || len(b.config.FloppyDirectories) > 0 { 228 err = errors.New("Generation 2 vms don't support floppy drives. Use ISO image instead.") 229 errs = packer.MultiErrorAppend(errs, err) 230 } 231 } 232 233 log.Println(fmt.Sprintf("Using switch %s", b.config.SwitchName)) 234 log.Println(fmt.Sprintf("%s: %v", "SwitchName", b.config.SwitchName)) 235 236 // Errors 237 if b.config.GuestAdditionsMode == "" { 238 if b.config.GuestAdditionsPath != "" { 239 b.config.GuestAdditionsMode = "attach" 240 } else { 241 b.config.GuestAdditionsPath = os.Getenv("WINDIR") + "\\system32\\vmguest.iso" 242 243 if _, err := os.Stat(b.config.GuestAdditionsPath); os.IsNotExist(err) { 244 if err != nil { 245 b.config.GuestAdditionsPath = "" 246 b.config.GuestAdditionsMode = "none" 247 } else { 248 b.config.GuestAdditionsMode = "attach" 249 } 250 } 251 } 252 } 253 254 if b.config.GuestAdditionsPath == "" && b.config.GuestAdditionsMode == "attach" { 255 b.config.GuestAdditionsPath = os.Getenv("WINDIR") + "\\system32\\vmguest.iso" 256 257 if _, err := os.Stat(b.config.GuestAdditionsPath); os.IsNotExist(err) { 258 if err != nil { 259 b.config.GuestAdditionsPath = "" 260 } 261 } 262 } 263 264 for _, isoPath := range b.config.SecondaryDvdImages { 265 if _, err := os.Stat(isoPath); os.IsNotExist(err) { 266 if err != nil { 267 errs = packer.MultiErrorAppend( 268 errs, fmt.Errorf("Secondary Dvd image does not exist: %s", err)) 269 } 270 } 271 } 272 273 numberOfIsos := len(b.config.SecondaryDvdImages) 274 275 if b.config.GuestAdditionsMode == "attach" { 276 if _, err := os.Stat(b.config.GuestAdditionsPath); os.IsNotExist(err) { 277 if err != nil { 278 errs = packer.MultiErrorAppend( 279 errs, fmt.Errorf("Guest additions iso does not exist: %s", err)) 280 } 281 } 282 283 numberOfIsos = numberOfIsos + 1 284 } 285 286 if b.config.Generation < 2 && numberOfIsos > 2 { 287 if b.config.GuestAdditionsMode == "attach" { 288 errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are only 2 ide controllers available, so "+ 289 "we can't support guest additions and these secondary dvds: %s", 290 strings.Join(b.config.SecondaryDvdImages, ", "))) 291 } else { 292 errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are only 2 ide controllers available, so "+ 293 "we can't support these secondary dvds: %s", 294 strings.Join(b.config.SecondaryDvdImages, ", "))) 295 } 296 } else if b.config.Generation > 1 && len(b.config.SecondaryDvdImages) > 16 { 297 if b.config.GuestAdditionsMode == "attach" { 298 errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are not enough drive letters available for "+ 299 "scsi (limited to 16), so we can't support guest additions and these secondary dvds: %s", 300 strings.Join(b.config.SecondaryDvdImages, ", "))) 301 } else { 302 errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are not enough drive letters available for "+ 303 "scsi (limited to 16), so we can't support these secondary dvds: %s", 304 strings.Join(b.config.SecondaryDvdImages, ", "))) 305 } 306 } 307 308 if b.config.EnableVirtualizationExtensions { 309 hasVirtualMachineVirtualizationExtensions, err := powershell.HasVirtualMachineVirtualizationExtensions() 310 if err != nil { 311 errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting virtual machine virtualization "+ 312 "extensions support: %s", err)) 313 } else { 314 if !hasVirtualMachineVirtualizationExtensions { 315 errs = packer.MultiErrorAppend(errs, fmt.Errorf("This version of Hyper-V does not support "+ 316 "virtual machine virtualization extension. Please use Windows 10 or Windows Server 2016 "+ 317 "or newer.")) 318 } 319 } 320 } 321 322 // Warnings 323 324 if b.config.ShutdownCommand == "" { 325 warnings = appendWarnings(warnings, 326 "A shutdown_command was not specified. Without a shutdown command, Packer\n"+ 327 "will forcibly halt the virtual machine, which may result in data loss.") 328 } 329 330 warning := b.checkHostAvailableMemory() 331 if warning != "" { 332 warnings = appendWarnings(warnings, warning) 333 } 334 335 if b.config.EnableVirtualizationExtensions { 336 if b.config.EnableDynamicMemory { 337 warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, " + 338 "dynamic memory should not be allowed.") 339 warnings = appendWarnings(warnings, warning) 340 } 341 342 if !b.config.EnableMacSpoofing { 343 warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, " + 344 "mac spoofing should be allowed.") 345 warnings = appendWarnings(warnings, warning) 346 } 347 348 if b.config.RamSize < MinNestedVirtualizationRamSize { 349 warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, " + 350 "there should be 4GB or more memory set for the vm, otherwise Hyper-V may fail to start " + 351 "any nested VMs.") 352 warnings = appendWarnings(warnings, warning) 353 } 354 } 355 356 if b.config.SwitchVlanId != "" { 357 if b.config.SwitchVlanId != b.config.VlanId { 358 warning = fmt.Sprintf("Switch network adaptor vlan should match virtual machine network adaptor " + 359 "vlan. The switch will not be able to see traffic from the VM.") 360 warnings = appendWarnings(warnings, warning) 361 } 362 } 363 364 if errs != nil && len(errs.Errors) > 0 { 365 return warnings, errs 366 } 367 368 return warnings, nil 369 } 370 371 // Run executes a Packer build and returns a packer.Artifact representing 372 // a Hyperv appliance. 373 func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { 374 // Create the driver that we'll use to communicate with Hyperv 375 driver, err := hypervcommon.NewHypervPS4Driver() 376 if err != nil { 377 return nil, fmt.Errorf("Failed creating Hyper-V driver: %s", err) 378 } 379 380 // Set up the state. 381 state := new(multistep.BasicStateBag) 382 state.Put("cache", cache) 383 state.Put("config", &b.config) 384 state.Put("debug", b.config.PackerDebug) 385 state.Put("driver", driver) 386 state.Put("hook", hook) 387 state.Put("ui", ui) 388 389 steps := []multistep.Step{ 390 &hypervcommon.StepCreateBuildDir{ 391 TempPath: b.config.TempPath, 392 }, 393 &hypervcommon.StepOutputDir{ 394 Force: b.config.PackerForce, 395 Path: b.config.OutputDir, 396 }, 397 } 398 399 if b.config.RawSingleISOUrl != "" || len(b.config.ISOUrls) > 0 { 400 steps = append(steps, 401 &common.StepDownload{ 402 Checksum: b.config.ISOChecksum, 403 ChecksumType: b.config.ISOChecksumType, 404 Description: "ISO", 405 ResultKey: "iso_path", 406 Url: b.config.ISOUrls, 407 Extension: b.config.TargetExtension, 408 TargetPath: b.config.TargetPath, 409 }, 410 ) 411 } 412 413 steps = append(steps, 414 &common.StepCreateFloppy{ 415 Files: b.config.FloppyFiles, 416 Directories: b.config.FloppyConfig.FloppyDirectories, 417 }, 418 &common.StepHTTPServer{ 419 HTTPDir: b.config.HTTPDir, 420 HTTPPortMin: b.config.HTTPPortMin, 421 HTTPPortMax: b.config.HTTPPortMax, 422 }, 423 &hypervcommon.StepCreateSwitch{ 424 SwitchName: b.config.SwitchName, 425 }, 426 &hypervcommon.StepCloneVM{ 427 CloneFromVMCXPath: b.config.CloneFromVMCXPath, 428 CloneFromVMName: b.config.CloneFromVMName, 429 CloneFromSnapshotName: b.config.CloneFromSnapshotName, 430 CloneAllSnapshots: b.config.CloneAllSnapshots, 431 VMName: b.config.VMName, 432 SwitchName: b.config.SwitchName, 433 RamSize: b.config.RamSize, 434 Cpu: b.config.Cpu, 435 EnableMacSpoofing: b.config.EnableMacSpoofing, 436 EnableDynamicMemory: b.config.EnableDynamicMemory, 437 EnableSecureBoot: b.config.EnableSecureBoot, 438 SecureBootTemplate: b.config.SecureBootTemplate, 439 EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions, 440 MacAddress: b.config.MacAddress, 441 }, 442 443 &hypervcommon.StepEnableIntegrationService{}, 444 445 &hypervcommon.StepMountDvdDrive{ 446 Generation: b.config.Generation, 447 }, 448 &hypervcommon.StepMountFloppydrive{ 449 Generation: b.config.Generation, 450 }, 451 452 &hypervcommon.StepMountGuestAdditions{ 453 GuestAdditionsMode: b.config.GuestAdditionsMode, 454 GuestAdditionsPath: b.config.GuestAdditionsPath, 455 Generation: b.config.Generation, 456 }, 457 458 &hypervcommon.StepMountSecondaryDvdImages{ 459 IsoPaths: b.config.SecondaryDvdImages, 460 Generation: b.config.Generation, 461 }, 462 463 &hypervcommon.StepConfigureVlan{ 464 VlanId: b.config.VlanId, 465 SwitchVlanId: b.config.SwitchVlanId, 466 }, 467 468 &hypervcommon.StepRun{ 469 Headless: b.config.Headless, 470 }, 471 472 &hypervcommon.StepTypeBootCommand{ 473 BootCommand: b.config.FlatBootCommand(), 474 BootWait: b.config.BootWait, 475 SwitchName: b.config.SwitchName, 476 Ctx: b.config.ctx, 477 GroupInterval: b.config.BootConfig.BootGroupInterval, 478 }, 479 480 // configure the communicator ssh, winrm 481 &communicator.StepConnect{ 482 Config: &b.config.SSHConfig.Comm, 483 Host: hypervcommon.CommHost, 484 SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(), 485 }, 486 487 // provision requires communicator to be setup 488 &common.StepProvision{}, 489 490 // Remove ephemeral SSH keys, if using 491 &common.StepCleanupTempKeys{ 492 Comm: &b.config.SSHConfig.Comm, 493 }, 494 495 &hypervcommon.StepShutdown{ 496 Command: b.config.ShutdownCommand, 497 Timeout: b.config.ShutdownTimeout, 498 }, 499 500 // wait for the vm to be powered off 501 &hypervcommon.StepWaitForPowerOff{}, 502 503 // remove the secondary dvd images 504 // after we power down 505 &hypervcommon.StepUnmountSecondaryDvdImages{}, 506 &hypervcommon.StepUnmountGuestAdditions{}, 507 &hypervcommon.StepUnmountDvdDrive{}, 508 &hypervcommon.StepUnmountFloppyDrive{ 509 Generation: b.config.Generation, 510 }, 511 &hypervcommon.StepCompactDisk{ 512 SkipCompaction: b.config.SkipCompaction, 513 }, 514 &hypervcommon.StepExportVm{ 515 OutputDir: b.config.OutputDir, 516 SkipExport: b.config.SkipExport, 517 }, 518 &hypervcommon.StepCollateArtifacts{ 519 OutputDir: b.config.OutputDir, 520 SkipExport: b.config.SkipExport, 521 }, 522 523 // the clean up actions for each step will be executed reverse order 524 ) 525 526 // Run the steps. 527 b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) 528 b.runner.Run(state) 529 530 // Report any errors. 531 if rawErr, ok := state.GetOk("error"); ok { 532 return nil, rawErr.(error) 533 } 534 535 // If we were interrupted or cancelled, then just exit. 536 if _, ok := state.GetOk(multistep.StateCancelled); ok { 537 return nil, errors.New("Build was cancelled.") 538 } 539 540 if _, ok := state.GetOk(multistep.StateHalted); ok { 541 return nil, errors.New("Build was halted.") 542 } 543 544 return hypervcommon.NewArtifact(b.config.OutputDir) 545 } 546 547 // Cancel. 548 func (b *Builder) Cancel() { 549 if b.runner != nil { 550 log.Println("Cancelling the step runner...") 551 b.runner.Cancel() 552 } 553 } 554 555 func appendWarnings(slice []string, data ...string) []string { 556 m := len(slice) 557 n := m + len(data) 558 if n > cap(slice) { // if necessary, reallocate 559 // allocate double what's needed, for future growth. 560 newSlice := make([]string, (n+1)*2) 561 copy(newSlice, slice) 562 slice = newSlice 563 } 564 slice = slice[0:n] 565 copy(slice[m:n], data) 566 return slice 567 } 568 569 func (b *Builder) checkRamSize() error { 570 if b.config.RamSize == 0 { 571 b.config.RamSize = DefaultRamSize 572 } 573 574 log.Println(fmt.Sprintf("%s: %v", "RamSize", b.config.RamSize)) 575 576 if b.config.RamSize < MinRamSize { 577 return fmt.Errorf("ram_size: Virtual machine requires memory size >= %v MB, but defined: %v", 578 MinRamSize, b.config.RamSize) 579 } else if b.config.RamSize > MaxRamSize { 580 return fmt.Errorf("ram_size: Virtual machine requires memory size <= %v MB, but defined: %v", 581 MaxRamSize, b.config.RamSize) 582 } 583 584 return nil 585 } 586 587 func (b *Builder) checkHostAvailableMemory() string { 588 powershellAvailable, _, _ := powershell.IsPowershellAvailable() 589 590 if powershellAvailable { 591 freeMB := powershell.GetHostAvailableMemory() 592 593 if (freeMB - float64(b.config.RamSize)) < LowRam { 594 return fmt.Sprintf("Hyper-V might fail to create a VM if there is not enough free memory in the system.") 595 } 596 } 597 598 return "" 599 } 600 601 func (b *Builder) detectSwitchName() string { 602 powershellAvailable, _, _ := powershell.IsPowershellAvailable() 603 604 if powershellAvailable { 605 // no switch name, try to get one attached to a online network adapter 606 onlineSwitchName, err := hyperv.GetExternalOnlineVirtualSwitch() 607 if onlineSwitchName != "" && err == nil { 608 return onlineSwitchName 609 } 610 } 611 612 return fmt.Sprintf("packer-%s", b.config.PackerBuildName) 613 }