diff --git a/builder/vmware/common/boot_config.go b/builder/vmware/common/boot_config.go new file mode 100644 index 000000000..970dfb80c --- /dev/null +++ b/builder/vmware/common/boot_config.go @@ -0,0 +1,42 @@ +//go:generate struct-markdown +package common + +import ( + "fmt" + + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/template/interpolate" +) + +type BootConfigWrapper struct { + bootcommand.VNCConfig `mapstructure:",squash"` + // If set to true, Packer will use USB HID Keyboard scan codes to send the boot command to the VM and + // the [disable_vnc](#disable_vnc) option will be ignored and automatically set to true. + // This option is not supported by hosts with free license. + // + // ~> **Note:** The ESXi 6.5+ removes support to VNC. In this case, the `usb_keyboard` should be set to true + // in order to send boot command keystrokes to the VM. + USBKeyBoard bool `mapstructure:"usb_keyboard"` +} + +func (c *BootConfigWrapper) Prepare(ctx *interpolate.Context, driverConfig *DriverConfig) (warnings []string, errs []error) { + if c.USBKeyBoard { + if driverConfig.RemoteType == "" { + warnings = append(warnings, "[WARN] `usb_keyboard` can only be used with remote VMWare builds. "+ + "The `usb_keyboard` option will be ignored and automatically set to false.") + c.USBKeyBoard = false + } else if !c.DisableVNC { + warnings = append(warnings, "[WARN] `usb_keyboard` is set to true then the remote VMWare builds "+ + "will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true.") + c.DisableVNC = true + return + } + } + + if len(c.BootCommand) > 0 && c.DisableVNC { + errs = append(errs, + fmt.Errorf("A boot command cannot be used when vnc is disabled.")) + } + errs = append(errs, c.BootConfig.Prepare(ctx)...) + return +} diff --git a/builder/vmware/common/boot_config_test.go b/builder/vmware/common/boot_config_test.go new file mode 100644 index 000000000..c4ea4e54b --- /dev/null +++ b/builder/vmware/common/boot_config_test.go @@ -0,0 +1,106 @@ +package common + +import ( + "fmt" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/template/interpolate" +) + +func TestVNCConfigWrapper_Prepare(t *testing.T) { + tc := []struct { + name string + config *BootConfigWrapper + expectedConfig *BootConfigWrapper + driver *DriverConfig + errs []error + warnings []string + }{ + { + name: "VNC and boot command for local build", + config: &BootConfigWrapper{ + VNCConfig: bootcommand.VNCConfig{ + BootConfig: bootcommand.BootConfig{ + BootCommand: []string{""}, + }, + DisableVNC: true, + }, + USBKeyBoard: false, + }, + expectedConfig: nil, + driver: new(DriverConfig), + errs: []error{fmt.Errorf("A boot command cannot be used when vnc is disabled.")}, + warnings: nil, + }, + { + name: "Disable VNC warning for remote build", + config: &BootConfigWrapper{ + VNCConfig: bootcommand.VNCConfig{ + BootConfig: bootcommand.BootConfig{ + BootCommand: []string{""}, + }, + DisableVNC: false, + }, + USBKeyBoard: true, + }, + expectedConfig: &BootConfigWrapper{ + VNCConfig: bootcommand.VNCConfig{ + DisableVNC: true, + }, + USBKeyBoard: true, + }, + driver: &DriverConfig{ + RemoteType: "esxi", + }, + errs: nil, + warnings: []string{"[WARN] `usb_keyboard` is set to true then the remote VMWare builds " + + "will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true."}, + }, + { + name: "Disable USBKeyBoard warning for local build", + config: &BootConfigWrapper{ + VNCConfig: bootcommand.VNCConfig{ + BootConfig: bootcommand.BootConfig{ + BootCommand: []string{""}, + }, + DisableVNC: false, + }, + USBKeyBoard: true, + }, + expectedConfig: &BootConfigWrapper{ + VNCConfig: bootcommand.VNCConfig{ + DisableVNC: false, + }, + USBKeyBoard: false, + }, + driver: &DriverConfig{}, + errs: nil, + warnings: []string{"[WARN] `usb_keyboard` can only be used with remote VMWare builds. " + + "The `usb_keyboard` option will be ignored and automatically set to false."}, + }, + } + + for _, c := range tc { + t.Run(c.name, func(t *testing.T) { + warnings, errs := c.config.Prepare(interpolate.NewContext(), c.driver) + if !reflect.DeepEqual(errs, c.errs) { + t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs) + } + if diff := cmp.Diff(warnings, c.warnings); diff != "" { + t.Fatalf("unexpected warnings: %s", diff) + } + if len(c.errs) == 0 { + if diff := cmp.Diff(c.config, c.expectedConfig, + cmpopts.IgnoreFields(bootcommand.VNCConfig{}, + "BootConfig", + )); diff != "" { + t.Fatalf("unexpected config: %s", diff) + } + } + }) + } +} diff --git a/builder/vmware/common/driver.go b/builder/vmware/common/driver.go index 167d3bce6..8149dbc33 100644 --- a/builder/vmware/common/driver.go +++ b/builder/vmware/common/driver.go @@ -91,20 +91,11 @@ func NewDriver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver, drivers := []Driver{} if dconfig.RemoteType != "" { - drivers = []Driver{ - &ESX5Driver{ - Host: dconfig.RemoteHost, - Port: dconfig.RemotePort, - Username: dconfig.RemoteUser, - Password: dconfig.RemotePassword, - PrivateKeyFile: dconfig.RemotePrivateKey, - Datastore: dconfig.RemoteDatastore, - CacheDatastore: dconfig.RemoteCacheDatastore, - CacheDirectory: dconfig.RemoteCacheDirectory, - VMName: vmName, - CommConfig: config.Comm, - }, + esx5Driver, err := NewESX5Driver(dconfig, config, vmName) + if err != nil { + return nil, err } + drivers = []Driver{esx5Driver} } else { switch runtime.GOOS { diff --git a/builder/vmware/common/driver_config.go b/builder/vmware/common/driver_config.go index c87ab3c6c..8e45ff293 100644 --- a/builder/vmware/common/driver_config.go +++ b/builder/vmware/common/driver_config.go @@ -52,6 +52,8 @@ type DriverConfig struct { } func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error { + var errs []error + if c.FusionAppPath == "" { c.FusionAppPath = os.Getenv("FUSION_APP_PATH") } @@ -74,7 +76,19 @@ func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error { c.RemotePort = 22 } - return nil + if c.RemoteType != "" { + if c.RemoteHost == "" { + errs = append(errs, + fmt.Errorf("remote_host must be specified")) + } + + if c.RemoteType != "esx5" { + errs = append(errs, + fmt.Errorf("Only 'esx5' value is accepted for remote_type")) + } + } + + return errs } func (c *DriverConfig) Validate(SkipExport bool) error { diff --git a/builder/vmware/common/driver_config_test.go b/builder/vmware/common/driver_config_test.go index 1f3774685..ccb7d545e 100644 --- a/builder/vmware/common/driver_config_test.go +++ b/builder/vmware/common/driver_config_test.go @@ -1,32 +1,84 @@ package common import ( + "fmt" + "reflect" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/packer/template/interpolate" ) func TestDriverConfigPrepare(t *testing.T) { - var c *DriverConfig - - // Test a default boot_wait - c = new(DriverConfig) - errs := c.Prepare(interpolate.NewContext()) - if len(errs) > 0 { - t.Fatalf("bad: %#v", errs) - } - if c.FusionAppPath != "/Applications/VMware Fusion.app" { - t.Fatalf("bad value: %s", c.FusionAppPath) + tc := []struct { + name string + config *DriverConfig + expectedConfig *DriverConfig + errs []error + }{ + { + name: "Set default values", + config: new(DriverConfig), + expectedConfig: &DriverConfig{ + FusionAppPath: "/Applications/VMware Fusion.app", + RemoteDatastore: "datastore1", + RemoteCacheDatastore: "datastore1", + RemoteCacheDirectory: "packer_cache", + RemotePort: 22, + RemoteUser: "root", + }, + errs: nil, + }, + { + name: "Override default values", + config: &DriverConfig{ + FusionAppPath: "foo", + RemoteDatastore: "set-datastore1", + RemoteCacheDatastore: "set-datastore1", + RemoteCacheDirectory: "set_packer_cache", + RemotePort: 443, + RemoteUser: "admin", + }, + expectedConfig: &DriverConfig{ + FusionAppPath: "foo", + RemoteDatastore: "set-datastore1", + RemoteCacheDatastore: "set-datastore1", + RemoteCacheDirectory: "set_packer_cache", + RemotePort: 443, + RemoteUser: "admin", + }, + errs: nil, + }, + { + name: "Invalid remote type", + config: &DriverConfig{ + RemoteType: "invalid", + RemoteHost: "host", + }, + expectedConfig: nil, + errs: []error{fmt.Errorf("Only 'esx5' value is accepted for remote_type")}, + }, + { + name: "Remote host not set", + config: &DriverConfig{ + RemoteType: "esx5", + }, + expectedConfig: nil, + errs: []error{fmt.Errorf("remote_host must be specified")}, + }, } - // Test with a good one - c = new(DriverConfig) - c.FusionAppPath = "foo" - errs = c.Prepare(interpolate.NewContext()) - if len(errs) > 0 { - t.Fatalf("bad: %#v", errs) - } - if c.FusionAppPath != "foo" { - t.Fatalf("bad value: %s", c.FusionAppPath) + for _, c := range tc { + t.Run(c.name, func(t *testing.T) { + errs := c.config.Prepare(interpolate.NewContext()) + if !reflect.DeepEqual(errs, c.errs) { + t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs) + } + if len(c.errs) == 0 { + if diff := cmp.Diff(c.config, c.expectedConfig); diff != "" { + t.Fatalf("bad value: %s", diff) + } + } + }) } } diff --git a/builder/vmware/common/driver_esx5.go b/builder/vmware/common/driver_esx5.go index dbece3ff9..64ce55349 100644 --- a/builder/vmware/common/driver_esx5.go +++ b/builder/vmware/common/driver_esx5.go @@ -26,7 +26,15 @@ import ( "github.com/hashicorp/packer/helper/multistep" helperssh "github.com/hashicorp/packer/helper/ssh" "github.com/hashicorp/packer/packer" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/session" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/methods" + "github.com/vmware/govmomi/vim25/soap" + "github.com/vmware/govmomi/vim25/types" gossh "golang.org/x/crypto/ssh" + "golang.org/x/mobile/event/key" ) // ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build @@ -45,11 +53,66 @@ type ESX5Driver struct { VMName string CommConfig communicator.Config + ctx context.Context + client *govmomi.Client + finder *find.Finder + comm packer.Communicator outputDir string vmId string } +func NewESX5Driver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver, error) { + ctx := context.TODO() + + vsphereUrl, err := url.Parse(fmt.Sprintf("https://%v/sdk", dconfig.RemoteHost)) + if err != nil { + return nil, err + } + credentials := url.UserPassword(dconfig.RemoteUser, dconfig.RemotePassword) + vsphereUrl.User = credentials + + soapClient := soap.NewClient(vsphereUrl, true) + vimClient, err := vim25.NewClient(ctx, soapClient) + if err != nil { + return nil, err + } + + vimClient.RoundTripper = session.KeepAlive(vimClient.RoundTripper, 10*time.Minute) + client := &govmomi.Client{ + Client: vimClient, + SessionManager: session.NewManager(vimClient), + } + + err = client.SessionManager.Login(ctx, credentials) + if err != nil { + return nil, err + } + + finder := find.NewFinder(client.Client, false) + datacenter, err := finder.DefaultDatacenter(ctx) + if err != nil { + return nil, err + } + finder.SetDatacenter(datacenter) + + return &ESX5Driver{ + Host: dconfig.RemoteHost, + Port: dconfig.RemotePort, + Username: dconfig.RemoteUser, + Password: dconfig.RemotePassword, + PrivateKeyFile: dconfig.RemotePrivateKey, + Datastore: dconfig.RemoteDatastore, + CacheDatastore: dconfig.RemoteCacheDatastore, + CacheDirectory: dconfig.RemoteCacheDirectory, + VMName: vmName, + CommConfig: config.Comm, + ctx: ctx, + client: client, + finder: finder, + }, nil +} + func (d *ESX5Driver) Clone(dst, src string, linked bool) error { linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\r\n"), "\n") } @@ -887,3 +950,38 @@ func (r *esxcliReader) find(key, val string) (map[string]string, error) { } } } + +type KeyInput struct { + Scancode key.Code + Alt bool + Ctrl bool + Shift bool +} + +func (d *ESX5Driver) TypeOnKeyboard(input KeyInput) (int32, error) { + vm, err := d.finder.VirtualMachine(d.ctx, d.VMName) + if err != nil { + return 0, err + } + + var spec types.UsbScanCodeSpec + spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{ + UsbHidCode: int32(input.Scancode)<<16 | 7, + Modifiers: &types.UsbScanCodeSpecModifierType{ + LeftControl: &input.Ctrl, + LeftAlt: &input.Alt, + LeftShift: &input.Shift, + }, + }) + + req := &types.PutUsbScanCodes{ + This: vm.Reference(), + Spec: spec, + } + + resp, err := methods.PutUsbScanCodes(d.ctx, d.client.RoundTripper, req) + if err != nil { + return 0, err + } + return resp.Returnval, nil +} diff --git a/builder/vmware/common/run_config.go b/builder/vmware/common/run_config.go index a649ae4c8..3abe25c2c 100644 --- a/builder/vmware/common/run_config.go +++ b/builder/vmware/common/run_config.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/packer/template/interpolate" ) +// ~> **Note:** If [usb_keyboard](#usb_keyboard) is set to true, any VNC configuration will be ignored. type RunConfig struct { // Packer defaults to building VMware virtual machines // by launching a GUI that shows the console of the machine being built. When @@ -39,25 +40,26 @@ type RunConfig struct { VNCDisablePassword bool `mapstructure:"vnc_disable_password" required:"false"` } -func (c *RunConfig) Prepare(ctx *interpolate.Context) (errs []error) { - if c.VNCPortMin == 0 { - c.VNCPortMin = 5900 - } +func (c *RunConfig) Prepare(_ *interpolate.Context, bootConfig *BootConfigWrapper) (errs []error) { + if !bootConfig.USBKeyBoard { + if c.VNCPortMin == 0 { + c.VNCPortMin = 5900 + } - if c.VNCPortMax == 0 { - c.VNCPortMax = 6000 - } + if c.VNCPortMax == 0 { + c.VNCPortMax = 6000 + } - if c.VNCBindAddress == "" { - c.VNCBindAddress = "127.0.0.1" - } + if c.VNCBindAddress == "" { + c.VNCBindAddress = "127.0.0.1" + } - if c.VNCPortMin > c.VNCPortMax { - errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) + if c.VNCPortMin > c.VNCPortMax { + errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) + } + if c.VNCPortMin < 0 { + errs = append(errs, fmt.Errorf("vnc_port_min must be positive")) + } } - if c.VNCPortMin < 0 { - errs = append(errs, fmt.Errorf("vnc_port_min must be positive")) - } - return } diff --git a/builder/vmware/common/step_usb_boot_command.go b/builder/vmware/common/step_usb_boot_command.go new file mode 100644 index 000000000..72ddbfb62 --- /dev/null +++ b/builder/vmware/common/step_usb_boot_command.go @@ -0,0 +1,126 @@ +package common + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + "golang.org/x/mobile/event/key" +) + +// This step "types" the boot command into the VM using USB Scan Codes. +type StepUSBBootCommand struct { + Config bootcommand.BootConfig + KeyInterval time.Duration + VMName string + Ctx interpolate.Context +} + +type USBBootCommandTemplateData struct { + HTTPIP string + HTTPPort int + Name string +} + +func (s *StepUSBBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + debug := state.Get("debug").(bool) + ui := state.Get("ui").(packer.Ui) + driver := state.Get("driver").(*ESX5Driver) + + if s.Config.BootCommand == nil { + return multistep.ActionContinue + } + + // Wait the for the vm to boot. + if int64(s.Config.BootWait) > 0 { + ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait.String())) + select { + case <-time.After(s.Config.BootWait): + break + case <-ctx.Done(): + return multistep.ActionHalt + } + } + + var pauseFn multistep.DebugPauseFn + if debug { + pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn) + } + + port := state.Get("http_port").(int) + if port > 0 { + ip := state.Get("http_ip").(string) + s.Ctx.Data = &USBBootCommandTemplateData{ + HTTPIP: ip, + HTTPPort: port, + Name: s.VMName, + } + ui.Say(fmt.Sprintf("HTTP server is working at http://%v:%v/", ip, port)) + } + + var keyAlt, keyCtrl, keyShift bool + sendCodes := func(code key.Code, down bool) error { + switch code { + case key.CodeLeftAlt: + keyAlt = down + case key.CodeLeftControl: + keyCtrl = down + case key.CodeLeftShift: + keyShift = down + } + + shift := down + if keyShift { + shift = keyShift + } + + _, err := driver.TypeOnKeyboard(KeyInput{ + Scancode: code, + Ctrl: keyCtrl, + Alt: keyAlt, + Shift: shift, + }) + if err != nil { + return fmt.Errorf("error typing a boot command (code, down) `%d, %t`: %w", code, down, err) + } + return nil + } + d := bootcommand.NewUSBDriver(sendCodes, s.KeyInterval) + + ui.Say("Typing boot command...") + flatBootCommand := s.Config.FlatBootCommand() + command, err := interpolate.Render(flatBootCommand, &s.Ctx) + if err != nil { + err := fmt.Errorf("Error preparing boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + seq, err := bootcommand.GenerateExpressionSequence(command) + if err != nil { + err := fmt.Errorf("Error generating boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if err := seq.Do(ctx, d); err != nil { + err := fmt.Errorf("Error running boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if pauseFn != nil { + pauseFn(multistep.DebugLocationAfterRun, fmt.Sprintf("boot_command: %s", command), state) + } + + return multistep.ActionContinue +} + +func (*StepUSBBootCommand) Cleanup(multistep.StateBag) {} diff --git a/builder/vmware/common/step_type_boot_command.go b/builder/vmware/common/step_vnc_boot_command.go similarity index 78% rename from builder/vmware/common/step_type_boot_command.go rename to builder/vmware/common/step_vnc_boot_command.go index 13baae9e2..2999bec84 100644 --- a/builder/vmware/common/step_type_boot_command.go +++ b/builder/vmware/common/step_vnc_boot_command.go @@ -23,22 +23,20 @@ import ( // // Produces: // -type StepTypeBootCommand struct { - BootCommand string - VNCEnabled bool - BootWait time.Duration - VMName string - Ctx interpolate.Context - KeyInterval time.Duration +type StepVNCBootCommand struct { + Config bootcommand.VNCConfig + VMName string + Ctx interpolate.Context } -type bootCommandTemplateData struct { + +type VNCBootCommandTemplateData struct { HTTPIP string HTTPPort int Name string } -func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - if !s.VNCEnabled { +func (s *StepVNCBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + if s.Config.DisableVNC { log.Println("Skipping boot command step...") return multistep.ActionContinue } @@ -51,10 +49,10 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) vncPassword := state.Get("vnc_password") // Wait the for the vm to boot. - if int64(s.BootWait) > 0 { - ui.Say(fmt.Sprintf("Waiting %s for boot...", s.BootWait.String())) + if int64(s.Config.BootWait) > 0 { + ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait.String())) select { - case <-time.After(s.BootWait): + case <-time.After(s.Config.BootWait): break case <-ctx.Done(): return multistep.ActionHalt @@ -98,16 +96,17 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) log.Printf("Connected to VNC desktop: %s", c.DesktopName) hostIP := state.Get("http_ip").(string) - s.Ctx.Data = &bootCommandTemplateData{ - hostIP, - httpPort, - s.VMName, + s.Ctx.Data = &VNCBootCommandTemplateData{ + HTTPIP: hostIP, + HTTPPort: httpPort, + Name: s.VMName, } - d := bootcommand.NewVNCDriver(c, s.KeyInterval) + d := bootcommand.NewVNCDriver(c, s.Config.BootKeyInterval) ui.Say("Typing the boot command over VNC...") - command, err := interpolate.Render(s.BootCommand, &s.Ctx) + flatBootCommand := s.Config.FlatBootCommand() + command, err := interpolate.Render(flatBootCommand, &s.Ctx) if err != nil { err := fmt.Errorf("Error preparing boot command: %s", err) state.Put("error", err) @@ -138,4 +137,4 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) return multistep.ActionContinue } -func (*StepTypeBootCommand) Cleanup(multistep.StateBag) {} +func (*StepVNCBootCommand) Cleanup(multistep.StateBag) {} diff --git a/builder/vmware/iso/builder.go b/builder/vmware/iso/builder.go index a57e2d5c8..f366f0a0d 100644 --- a/builder/vmware/iso/builder.go +++ b/builder/vmware/iso/builder.go @@ -52,6 +52,20 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack state.Put("driverConfig", &b.config.DriverConfig) state.Put("temporaryDevices", []string{}) // Devices (in .vmx) created by packer during building + var stepBootCommand multistep.Step = &vmwcommon.StepVNCBootCommand{ + Config: b.config.VNCConfig, + VMName: b.config.VMName, + Ctx: b.config.ctx, + } + if b.config.USBKeyBoard { + stepBootCommand = &vmwcommon.StepUSBBootCommand{ + Config: b.config.VNCConfig.BootConfig, + KeyInterval: b.config.VNCConfig.BootKeyInterval, + VMName: b.config.VMName, + Ctx: b.config.ctx, + } + } + steps := []multistep.Step{ &vmwcommon.StepPrepareTools{ RemoteType: b.config.RemoteType, @@ -137,14 +151,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack DurationBeforeStop: 5 * time.Second, Headless: b.config.Headless, }, - &vmwcommon.StepTypeBootCommand{ - BootWait: b.config.BootWait, - VNCEnabled: !b.config.DisableVNC, - BootCommand: b.config.FlatBootCommand(), - VMName: b.config.VMName, - Ctx: b.config.ctx, - KeyInterval: b.config.VNCConfig.BootKeyInterval, - }, + stepBootCommand, &communicator.StepConnect{ Config: &b.config.SSHConfig.Comm, Host: driver.CommHost, diff --git a/builder/vmware/iso/config.go b/builder/vmware/iso/config.go index 25c987d99..319d5fdb5 100644 --- a/builder/vmware/iso/config.go +++ b/builder/vmware/iso/config.go @@ -11,7 +11,6 @@ import ( vmwcommon "github.com/hashicorp/packer/builder/vmware/common" "github.com/hashicorp/packer/common" - "github.com/hashicorp/packer/common/bootcommand" "github.com/hashicorp/packer/common/shutdowncommand" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" @@ -24,7 +23,7 @@ type Config struct { common.ISOConfig `mapstructure:",squash"` common.FloppyConfig `mapstructure:",squash"` common.CDConfig `mapstructure:",squash"` - bootcommand.VNCConfig `mapstructure:",squash"` + vmwcommon.BootConfigWrapper `mapstructure:",squash"` vmwcommon.DriverConfig `mapstructure:",squash"` vmwcommon.HWConfig `mapstructure:",squash"` vmwcommon.OutputConfig `mapstructure:",squash"` @@ -94,25 +93,27 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { } // Accumulate any errors and warnings + var warnings []string var errs *packer.MultiError - warnings := make([]string, 0) + + vncWarnings, vncErrs := c.BootConfigWrapper.Prepare(&c.ctx, &c.DriverConfig) + warnings = append(warnings, vncWarnings...) + errs = packer.MultiErrorAppend(errs, vncErrs...) isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx) warnings = append(warnings, isoWarnings...) errs = packer.MultiErrorAppend(errs, isoErrs...) errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.HWConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...) errs = packer.MultiErrorAppend(errs, c.DriverConfig.Prepare(&c.ctx)...) - errs = packer.MultiErrorAppend(errs, - c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...) - errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx, &c.BootConfigWrapper)...) errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...) - errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...) - errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.DiskConfig.Prepare(&c.ctx)...) @@ -165,19 +166,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { c.HWConfig.Network = "nat" } - // Remote configuration validation - if c.RemoteType != "" { - if c.RemoteHost == "" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("remote_host must be specified")) - } - - if c.RemoteType != "esx5" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("Only 'esx5' value is accepted for remote_type")) - } - } - if c.Format == "" { if c.RemoteType == "" { c.Format = "vmx" @@ -186,11 +174,18 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { } } - if c.RemoteType == "" && c.Format == "vmx" { - // if we're building locally and want a vmx, there's nothing to export. - // Set skip export flag here to keep the export step from attempting - // an unneded export - c.SkipExport = true + if c.RemoteType == "" { + if c.Format == "vmx" { + // if we're building locally and want a vmx, there's nothing to export. + // Set skip export flag here to keep the export step from attempting + // an unneded export + c.SkipExport = true + } + if c.Headless && c.DisableVNC { + warnings = append(warnings, + "Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+ + "you won't be able to see any output.") + } } err = c.DriverConfig.Validate(c.SkipExport) @@ -198,19 +193,12 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, err) } - // Warnings if c.ShutdownCommand == "" { warnings = append(warnings, "A shutdown_command was not specified. Without a shutdown command, Packer\n"+ "will forcibly halt the virtual machine, which may result in data loss.") } - if c.Headless && c.DisableVNC { - warnings = append(warnings, - "Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+ - "you won't be able to see any output.") - } - if errs != nil && len(errs.Errors) > 0 { return warnings, errs } diff --git a/builder/vmware/iso/config.hcl2spec.go b/builder/vmware/iso/config.hcl2spec.go index 345eb4c38..a8affc031 100644 --- a/builder/vmware/iso/config.hcl2spec.go +++ b/builder/vmware/iso/config.hcl2spec.go @@ -35,6 +35,7 @@ type FlatConfig struct { BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` DisableVNC *bool `mapstructure:"disable_vnc" cty:"disable_vnc" hcl:"disable_vnc"` BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"` + USBKeyBoard *bool `mapstructure:"usb_keyboard" cty:"usb_keyboard" hcl:"usb_keyboard"` CleanUpRemoteCache *bool `mapstructure:"cleanup_remote_cache" required:"false" cty:"cleanup_remote_cache" hcl:"cleanup_remote_cache"` FusionAppPath *string `mapstructure:"fusion_app_path" required:"false" cty:"fusion_app_path" hcl:"fusion_app_path"` RemoteType *string `mapstructure:"remote_type" required:"false" cty:"remote_type" hcl:"remote_type"` @@ -176,6 +177,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false}, "disable_vnc": &hcldec.AttrSpec{Name: "disable_vnc", Type: cty.Bool, Required: false}, "boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false}, + "usb_keyboard": &hcldec.AttrSpec{Name: "usb_keyboard", Type: cty.Bool, Required: false}, "cleanup_remote_cache": &hcldec.AttrSpec{Name: "cleanup_remote_cache", Type: cty.Bool, Required: false}, "fusion_app_path": &hcldec.AttrSpec{Name: "fusion_app_path", Type: cty.String, Required: false}, "remote_type": &hcldec.AttrSpec{Name: "remote_type", Type: cty.String, Required: false}, diff --git a/builder/vmware/vmx/builder.go b/builder/vmware/vmx/builder.go index 4c303d8bb..bf216a60a 100644 --- a/builder/vmware/vmx/builder.go +++ b/builder/vmware/vmx/builder.go @@ -56,6 +56,20 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack state.Put("driverConfig", &b.config.DriverConfig) state.Put("temporaryDevices", []string{}) // Devices (in .vmx) created by packer during building + var stepBootCommand multistep.Step = &vmwcommon.StepVNCBootCommand{ + Config: b.config.VNCConfig, + VMName: b.config.VMName, + Ctx: b.config.ctx, + } + if b.config.USBKeyBoard { + stepBootCommand = &vmwcommon.StepUSBBootCommand{ + Config: b.config.VNCConfig.BootConfig, + KeyInterval: b.config.VNCConfig.BootKeyInterval, + VMName: b.config.VMName, + Ctx: b.config.ctx, + } + } + // Build the steps. steps := []multistep.Step{ &vmwcommon.StepPrepareTools{ @@ -126,14 +140,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack DurationBeforeStop: 5 * time.Second, Headless: b.config.Headless, }, - &vmwcommon.StepTypeBootCommand{ - BootWait: b.config.BootWait, - VNCEnabled: !b.config.DisableVNC, - BootCommand: b.config.FlatBootCommand(), - VMName: b.config.VMName, - Ctx: b.config.ctx, - KeyInterval: b.config.VNCConfig.BootKeyInterval, - }, + stepBootCommand, &communicator.StepConnect{ Config: &b.config.SSHConfig.Comm, Host: driver.CommHost, @@ -203,5 +210,3 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath, b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state) } - -// Cancel. diff --git a/builder/vmware/vmx/config.go b/builder/vmware/vmx/config.go index fc32099e1..269de35ce 100644 --- a/builder/vmware/vmx/config.go +++ b/builder/vmware/vmx/config.go @@ -9,7 +9,6 @@ import ( vmwcommon "github.com/hashicorp/packer/builder/vmware/common" "github.com/hashicorp/packer/common" - "github.com/hashicorp/packer/common/bootcommand" "github.com/hashicorp/packer/common/shutdowncommand" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" @@ -21,7 +20,7 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` common.HTTPConfig `mapstructure:",squash"` common.FloppyConfig `mapstructure:",squash"` - bootcommand.VNCConfig `mapstructure:",squash"` + vmwcommon.BootConfigWrapper `mapstructure:",squash"` vmwcommon.DriverConfig `mapstructure:",squash"` vmwcommon.OutputConfig `mapstructure:",squash"` vmwcommon.RunConfig `mapstructure:",squash"` @@ -77,16 +76,20 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { "packer-%s-%d", c.PackerBuildName, interpolate.InitTime.Unix()) } - // Prepare the errors + // Accumulate any errors and warnings + var warnings []string var errs *packer.MultiError + + vncWarnings, vncErrs := c.BootConfigWrapper.Prepare(&c.ctx, &c.DriverConfig) + warnings = append(warnings, vncWarnings...) + errs = packer.MultiErrorAppend(errs, vncErrs...) errs = packer.MultiErrorAppend(errs, c.DriverConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...) - errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx, &c.BootConfigWrapper)...) errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...) - errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...) @@ -101,16 +104,10 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { fmt.Errorf("source_path is invalid: %s", err)) } } - } else { - // Remote configuration validation - if c.RemoteHost == "" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("remote_host must be specified")) - } - - if c.RemoteType != "esx5" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("Only 'esx5' value is accepted for remote_type")) + if c.Headless && c.DisableVNC { + warnings = append(warnings, + "Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+ + "you won't be able to see any output.") } } @@ -143,20 +140,12 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, err) } - // Warnings - var warnings []string if c.ShutdownCommand == "" { warnings = append(warnings, "A shutdown_command was not specified. Without a shutdown command, Packer\n"+ "will forcibly halt the virtual machine, which may result in data loss.") } - if c.Headless && c.DisableVNC { - warnings = append(warnings, - "Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+ - "you won't be able to see any output.") - } - // Check for any errors. if errs != nil && len(errs.Errors) > 0 { return warnings, errs diff --git a/builder/vmware/vmx/config.hcl2spec.go b/builder/vmware/vmx/config.hcl2spec.go index c507035a6..16e047de1 100644 --- a/builder/vmware/vmx/config.hcl2spec.go +++ b/builder/vmware/vmx/config.hcl2spec.go @@ -28,6 +28,7 @@ type FlatConfig struct { BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` DisableVNC *bool `mapstructure:"disable_vnc" cty:"disable_vnc" hcl:"disable_vnc"` BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"` + USBKeyBoard *bool `mapstructure:"usb_keyboard" cty:"usb_keyboard" hcl:"usb_keyboard"` CleanUpRemoteCache *bool `mapstructure:"cleanup_remote_cache" required:"false" cty:"cleanup_remote_cache" hcl:"cleanup_remote_cache"` FusionAppPath *string `mapstructure:"fusion_app_path" required:"false" cty:"fusion_app_path" hcl:"fusion_app_path"` RemoteType *string `mapstructure:"remote_type" required:"false" cty:"remote_type" hcl:"remote_type"` @@ -148,6 +149,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false}, "disable_vnc": &hcldec.AttrSpec{Name: "disable_vnc", Type: cty.Bool, Required: false}, "boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false}, + "usb_keyboard": &hcldec.AttrSpec{Name: "usb_keyboard", Type: cty.Bool, Required: false}, "cleanup_remote_cache": &hcldec.AttrSpec{Name: "cleanup_remote_cache", Type: cty.Bool, Required: false}, "fusion_app_path": &hcldec.AttrSpec{Name: "fusion_app_path", Type: cty.String, Required: false}, "remote_type": &hcldec.AttrSpec{Name: "remote_type", Type: cty.String, Required: false}, diff --git a/website/pages/docs/builders/vmware/iso.mdx b/website/pages/docs/builders/vmware/iso.mdx index fed7d1bc1..d691848f1 100644 --- a/website/pages/docs/builders/vmware/iso.mdx +++ b/website/pages/docs/builders/vmware/iso.mdx @@ -159,6 +159,8 @@ necessary for this build to succeed and can be found further down the page. ### Run configuration +@include 'builder/vmware/common/RunConfig.mdx' + #### Optional: @include 'builder/vmware/common/RunConfig-not-required.mdx' @@ -199,6 +201,8 @@ necessary for this build to succeed and can be found further down the page. @include 'common/bootcommand/BootConfig.mdx' +@include 'builder/vmware/common/BootConfigWrapper-not-required.mdx' + @include 'common/bootcommand/VNCConfig.mdx' -> **Note**: for the `HTTPIP` to be resolved correctly, your VM's network diff --git a/website/pages/docs/builders/vmware/vmx.mdx b/website/pages/docs/builders/vmware/vmx.mdx index 71fad1a0f..6f8640503 100644 --- a/website/pages/docs/builders/vmware/vmx.mdx +++ b/website/pages/docs/builders/vmware/vmx.mdx @@ -133,6 +133,8 @@ necessary for this build to succeed and can be found further down the page. ### Run configuration +@include 'builder/vmware/common/RunConfig.mdx' + #### Optional: @include 'builder/vmware/common/RunConfig-not-required.mdx' @@ -177,6 +179,8 @@ necessary for this build to succeed and can be found further down the page. @include 'common/bootcommand/BootConfig.mdx' +@include 'builder/vmware/common/BootConfigWrapper-not-required.mdx' + @include 'common/bootcommand/VNCConfig.mdx' -> **Note**: for the `HTTPIP` to be resolved correctly, your VM's network @@ -188,6 +192,7 @@ to modify the network configuration after the VM is done building. #### Optional: @include 'common/bootcommand/VNCConfig-not-required.mdx' + @include 'common/bootcommand/BootConfig-not-required.mdx' For more examples of various boot commands, see the sample projects from our @@ -198,7 +203,7 @@ For more examples of various boot commands, see the sample projects from our "builders": [ { "type": "vmware-vmx", - "boot_key_interval": "10ms" + "boot_key_interval": "10ms", ... } ] diff --git a/website/pages/partials/builder/vmware/common/BootConfigWrapper-not-required.mdx b/website/pages/partials/builder/vmware/common/BootConfigWrapper-not-required.mdx new file mode 100644 index 000000000..66f458040 --- /dev/null +++ b/website/pages/partials/builder/vmware/common/BootConfigWrapper-not-required.mdx @@ -0,0 +1,8 @@ + + +- `usb_keyboard` (bool) - If set to true, Packer will use USB HID Keyboard scan codes to send the boot command to the VM and + the [disable_vnc](#disable_vnc) option will be ignored and automatically set to true. + This option is not supported by hosts with free license. + + ~> **Note:** The ESXi 6.5+ removes support to VNC. In this case, the `usb_keyboard` should be set to true + in order to send boot command keystrokes to the VM. diff --git a/website/pages/partials/builder/vmware/common/RunConfig.mdx b/website/pages/partials/builder/vmware/common/RunConfig.mdx new file mode 100644 index 000000000..07dee5184 --- /dev/null +++ b/website/pages/partials/builder/vmware/common/RunConfig.mdx @@ -0,0 +1,3 @@ + + +~> **Note:** If [usb_keyboard](#usb_keyboard) is set to true, any VNC configuration will be ignored.