Add usb_scan_codes option to use USB scancodes instead of VNC for remote builds (#9895)

This commit is contained in:
Sylvia Moss 2020-09-11 16:46:33 +02:00 committed by GitHub
parent 2837d56885
commit 31f08909b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 587 additions and 144 deletions

View File

@ -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
}

View File

@ -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{"<boot><command>"},
},
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{"<boot><command>"},
},
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{"<boot><command>"},
},
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)
}
}
})
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
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 c.FusionAppPath != "foo" {
t.Fatalf("bad value: %s", c.FusionAppPath)
if len(c.errs) == 0 {
if diff := cmp.Diff(c.config, c.expectedConfig); diff != "" {
t.Fatalf("bad value: %s", diff)
}
}
})
}
}

View File

@ -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
}

View File

@ -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,7 +40,8 @@ type RunConfig struct {
VNCDisablePassword bool `mapstructure:"vnc_disable_password" required:"false"`
}
func (c *RunConfig) Prepare(ctx *interpolate.Context) (errs []error) {
func (c *RunConfig) Prepare(_ *interpolate.Context, bootConfig *BootConfigWrapper) (errs []error) {
if !bootConfig.USBKeyBoard {
if c.VNCPortMin == 0 {
c.VNCPortMin = 5900
}
@ -58,6 +60,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) (errs []error) {
if c.VNCPortMin < 0 {
errs = append(errs, fmt.Errorf("vnc_port_min must be positive"))
}
}
return
}

View File

@ -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) {}

View File

@ -23,22 +23,20 @@ import (
//
// Produces:
// <nothing>
type StepTypeBootCommand struct {
BootCommand string
VNCEnabled bool
BootWait time.Duration
type StepVNCBootCommand struct {
Config bootcommand.VNCConfig
VMName string
Ctx interpolate.Context
KeyInterval time.Duration
}
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) {}

View File

@ -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,

View File

@ -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,31 +174,31 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
}
if c.RemoteType == "" && c.Format == "vmx" {
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)
if err != nil {
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
}

View File

@ -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},

View File

@ -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.

View File

@ -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

View File

@ -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},

View File

@ -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

View File

@ -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",
...
}
]

View File

@ -0,0 +1,8 @@
<!-- Code generated from the comments of the BootConfigWrapper struct in builder/vmware/common/boot_config.go; DO NOT EDIT MANUALLY -->
- `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.

View File

@ -0,0 +1,3 @@
<!-- Code generated from the comments of the RunConfig struct in builder/vmware/common/run_config.go; DO NOT EDIT MANUALLY -->
~> **Note:** If [usb_keyboard](#usb_keyboard) is set to true, any VNC configuration will be ignored.