From 585a86fe03a9be562393dc66f2457b21cd3cf9ea Mon Sep 17 00:00:00 2001 From: Moss Date: Thu, 11 Jun 2020 18:37:32 +0200 Subject: [PATCH] Add usb_driver to common boot_command and use it on vsphere --- builder/vsphere/common/StepHTTPIPDiscover.go | 54 ++++ builder/vsphere/driver/vm_keyboard.go | 56 +--- builder/vsphere/iso/builder.go | 4 + builder/vsphere/iso/config.go | 2 +- builder/vsphere/iso/step_boot_command.go | 271 ++++++------------ builder/vsphere/iso/step_boot_command_test.go | 53 ++++ common/bootcommand/usb_driver.go | 186 ++++++++++++ 7 files changed, 381 insertions(+), 245 deletions(-) create mode 100644 builder/vsphere/common/StepHTTPIPDiscover.go create mode 100644 builder/vsphere/iso/step_boot_command_test.go create mode 100644 common/bootcommand/usb_driver.go diff --git a/builder/vsphere/common/StepHTTPIPDiscover.go b/builder/vsphere/common/StepHTTPIPDiscover.go new file mode 100644 index 000000000..627fbe52b --- /dev/null +++ b/builder/vsphere/common/StepHTTPIPDiscover.go @@ -0,0 +1,54 @@ +package common + +import ( + "context" + "fmt" + "net" + + "github.com/hashicorp/packer/helper/multistep" +) + +// Step to discover the http ip +// which guests use to reach the vm host +// To make sure the IP is set before boot command and http server steps +type StepHTTPIPDiscover struct { + HTTPIP string +} + +func (s *StepHTTPIPDiscover) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ip, err := getHostIP(s.HTTPIP) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + state.Put("http_ip", ip) + + return multistep.ActionContinue +} + +func (s *StepHTTPIPDiscover) Cleanup(state multistep.StateBag) {} + +func getHostIP(s string) (string, error) { + if s != "" { + if net.ParseIP(s) != nil { + return s, nil + } else { + return "", fmt.Errorf("invalid IP address") + } + } + + addrs, err := net.InterfaceAddrs() + if err != nil { + return "", err + } + + for _, a := range addrs { + ipnet, ok := a.(*net.IPNet) + if ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String(), nil + } + } + } + return "", fmt.Errorf("IP not found") +} diff --git a/builder/vsphere/driver/vm_keyboard.go b/builder/vsphere/driver/vm_keyboard.go index 616ec958c..4cf41af5b 100644 --- a/builder/vsphere/driver/vm_keyboard.go +++ b/builder/vsphere/driver/vm_keyboard.go @@ -1,9 +1,6 @@ package driver import ( - "strings" - "unicode" - "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/types" "golang.org/x/mobile/event/key" @@ -17,58 +14,7 @@ type KeyInput struct { Shift bool } -var scancodeMap = make(map[rune]key.Code) - -func init() { - scancodeIndex := make(map[string]key.Code) - scancodeIndex["abcdefghijklmnopqrstuvwxyz"] = key.CodeA - scancodeIndex["ABCDEFGHIJKLMNOPQRSTUVWXYZ"] = key.CodeA - scancodeIndex["1234567890"] = key.Code1 - scancodeIndex["!@#$%^&*()"] = key.Code1 - scancodeIndex[" "] = key.CodeSpacebar - scancodeIndex["-=[]\\"] = key.CodeHyphenMinus - scancodeIndex["_+{}|"] = key.CodeHyphenMinus - scancodeIndex[";'`,./"] = key.CodeSemicolon - scancodeIndex[":\"~<>?"] = key.CodeSemicolon - - for chars, start := range scancodeIndex { - for i, r := range chars { - scancodeMap[r] = start + key.Code(i) - } - } -} - -const shiftedChars = "!@#$%^&*()_+{}|:\"~<>?" - -func (vm *VirtualMachine) TypeOnKeyboard(input KeyInput) (int32, error) { - var spec types.UsbScanCodeSpec - - for _, r := range input.Message { - scancode := scancodeMap[r] - shift := input.Shift || unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r) - - spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{ - // https://github.com/lamw/vghetto-scripts/blob/f74bc8ba20064f46592bcce5a873b161a7fa3d72/powershell/VMKeystrokes.ps1#L130 - UsbHidCode: int32(scancode)<<16 | 7, - Modifiers: &types.UsbScanCodeSpecModifierType{ - LeftControl: &input.Ctrl, - LeftAlt: &input.Alt, - LeftShift: &shift, - }, - }) - } - - if input.Scancode != 0 { - 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, - }, - }) - } - +func (vm *VirtualMachine) TypeOnKeyboard(spec types.UsbScanCodeSpec) (int32, error) { req := &types.PutUsbScanCodes{ This: vm.vm.Reference(), Spec: spec, diff --git a/builder/vsphere/iso/builder.go b/builder/vsphere/iso/builder.go index 3bba2aa83..2c224b642 100644 --- a/builder/vsphere/iso/builder.go +++ b/builder/vsphere/iso/builder.go @@ -30,6 +30,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { state := new(multistep.BasicStateBag) + state.Put("debug", b.config.PackerDebug) state.Put("hook", hook) state.Put("ui", ui) @@ -89,6 +90,9 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack Host: b.config.Host, SetHostForDatastoreUploads: b.config.SetHostForDatastoreUploads, }, + &common.StepHTTPIPDiscover{ + HTTPIP: b.config.BootConfig.HTTPIP, + }, &packerCommon.StepHTTPServer{ HTTPDir: b.config.HTTPDir, HTTPPortMin: b.config.HTTPPortMin, diff --git a/builder/vsphere/iso/config.go b/builder/vsphere/iso/config.go index 7be966781..a705d329e 100644 --- a/builder/vsphere/iso/config.go +++ b/builder/vsphere/iso/config.go @@ -76,7 +76,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.CDRomConfig.Prepare()...) - errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare()...) + errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.WaitIpConfig.Prepare()...) errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare()...) diff --git a/builder/vsphere/iso/step_boot_command.go b/builder/vsphere/iso/step_boot_command.go index 70c5adc7c..aaaf58d94 100644 --- a/builder/vsphere/iso/step_boot_command.go +++ b/builder/vsphere/iso/step_boot_command.go @@ -3,25 +3,19 @@ package iso import ( "context" "fmt" - "log" - "net" - "os" - "strings" - "time" - "unicode/utf8" - "github.com/hashicorp/packer/builder/vsphere/driver" - packerCommon "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/bootcommand" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" + "github.com/vmware/govmomi/vim25/types" "golang.org/x/mobile/event/key" + "time" ) type BootConfig struct { - BootCommand []string `mapstructure:"boot_command"` - BootWait time.Duration `mapstructure:"boot_wait"` // example: "1m30s"; default: "10s" - HTTPIP string `mapstructure:"http_ip"` + bootcommand.BootConfig `mapstructure:",squash"` + HTTPIP string `mapstructure:"http_ip"` } type bootCommandTemplateData struct { @@ -30,13 +24,15 @@ type bootCommandTemplateData struct { Name string } -func (c *BootConfig) Prepare() []error { +func (c *BootConfig) Prepare(ctx *interpolate.Context) []error { var errs []error if c.BootWait == 0 { c.BootWait = 10 * time.Second } + c.BootConfig.Prepare(ctx) + return errs } @@ -46,44 +42,8 @@ type StepBootCommand struct { Ctx interpolate.Context } -var special = map[string]key.Code{ - "": key.CodeReturnEnter, - "": key.CodeEscape, - "": key.CodeDeleteBackspace, - "": key.CodeDeleteForward, - "": key.CodeTab, - "": key.CodeF1, - "": key.CodeF2, - "": key.CodeF3, - "": key.CodeF4, - "": key.CodeF5, - "": key.CodeF6, - "": key.CodeF7, - "": key.CodeF8, - "": key.CodeF9, - "": key.CodeF10, - "": key.CodeF11, - "": key.CodeF12, - "": key.CodeInsert, - "": key.CodeHome, - "": key.CodeEnd, - "": key.CodePageUp, - "": key.CodePageDown, - "": key.CodeLeftArrow, - "": key.CodeRightArrow, - "": key.CodeUpArrow, - "": key.CodeDownArrow, -} - -var keyInterval = packerCommon.PackerKeyDefault - -func init() { - if delay, err := time.ParseDuration(os.Getenv(packerCommon.PackerKeyEnv)); err == nil { - keyInterval = delay - } -} - -func (s *StepBootCommand) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { +func (s *StepBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + debug := state.Get("debug").(bool) ui := state.Get("ui").(packer.Ui) vm := state.Get("vm").(*driver.VirtualMachine) @@ -91,28 +51,25 @@ func (s *StepBootCommand) Run(_ context.Context, state multistep.StateBag) multi return multistep.ActionContinue } - ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait)) - wait := time.After(s.Config.BootWait) -WAITLOOP: - for { + // 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 <-wait: - break WAITLOOP - case <-time.After(1 * time.Second): - if _, ok := state.GetOk(multistep.StateCancelled); ok { - return multistep.ActionHalt - } + 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, err := getHostIP(s.Config.HTTPIP) - if err != nil { - state.Put("error", err) - return multistep.ActionHalt - } - state.Put("http_ip", ip) + ip := state.Get("http_ip").(string) s.Ctx.Data = &bootCommandTemplateData{ ip, port, @@ -121,136 +78,72 @@ WAITLOOP: ui.Say(fmt.Sprintf("HTTP server is working at http://%v:%v/", ip, port)) } - ui.Say("Typing boot command...") - var keyAlt bool - var keyCtrl bool - var keyShift bool - for _, command := range s.Config.BootCommand { - message, err := interpolate.Render(command, &s.Ctx) - if err != nil { - state.Put("error", err) - return multistep.ActionHalt - } + sendCodes := func(codes []key.Code, downs []bool) error { + var spec types.UsbScanCodeSpec - for len(message) > 0 { - if _, ok := state.GetOk(multistep.StateCancelled); ok { - return multistep.ActionHalt + for i, code := range codes { + var keyAlt, keyCtrl, keyShift bool + + switch code { + case key.CodeLeftAlt: + // + keyAlt = downs[i] + case key.CodeLeftControl: + // + keyCtrl = downs[i] + case key.CodeLeftShift: + // + keyShift = downs[i] } - if strings.HasPrefix(message, "") { - log.Printf("Waiting 1 second") - time.Sleep(1 * time.Second) - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - log.Printf("Waiting 5 seconds") - time.Sleep(5 * time.Second) - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - log.Printf("Waiting 10 seconds") - time.Sleep(10 * time.Second) - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - keyAlt = true - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - keyAlt = false - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - keyCtrl = true - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - keyCtrl = false - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - keyShift = true - message = message[len(""):] - continue - } - - if strings.HasPrefix(message, "") { - keyShift = false - message = message[len(""):] - continue - } - - var scancode key.Code - for specialCode, specialValue := range special { - if strings.HasPrefix(message, specialCode) { - scancode = specialValue - log.Printf("Special code '%s' found, replacing with: %s", specialCode, specialValue) - message = message[len(specialCode):] - } - } - - var char rune - if scancode == 0 { - var size int - char, size = utf8.DecodeRuneInString(message) - message = message[size:] - } - - _, err := vm.TypeOnKeyboard(driver.KeyInput{ - Message: string(char), - Scancode: scancode, - Ctrl: keyCtrl, - Alt: keyAlt, - Shift: keyShift, + spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{ + UsbHidCode: int32(code)<<16 | 7, + Modifiers: &types.UsbScanCodeSpecModifierType{ + LeftControl: &keyCtrl, + LeftAlt: &keyAlt, + LeftShift: &keyShift, + }, }) - if err != nil { - state.Put("error", fmt.Errorf("error typing a boot command: %v", err)) - return multistep.ActionHalt - } - time.Sleep(keyInterval) } + + _, err := vm.TypeOnKeyboard(spec) + if err != nil { + return fmt.Errorf("error typing a boot command: %v", err) + } + return nil + } + d := bootcommand.NewUSBDriver(sendCodes, s.Config.BootGroupInterval) + + 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 (s *StepBootCommand) Cleanup(state multistep.StateBag) {} - -func getHostIP(s string) (string, error) { - if s != "" { - if net.ParseIP(s) != nil { - return s, nil - } else { - return "", fmt.Errorf("invalid IP address") - } - } - - addrs, err := net.InterfaceAddrs() - if err != nil { - return "", err - } - - for _, a := range addrs { - ipnet, ok := a.(*net.IPNet) - if ok && !ipnet.IP.IsLoopback() { - if ipnet.IP.To4() != nil { - return ipnet.IP.String(), nil - } - } - } - return "", fmt.Errorf("IP not found") -} +func (s *StepBootCommand) Cleanup(_ multistep.StateBag) {} diff --git a/builder/vsphere/iso/step_boot_command_test.go b/builder/vsphere/iso/step_boot_command_test.go new file mode 100644 index 000000000..beac28350 --- /dev/null +++ b/builder/vsphere/iso/step_boot_command_test.go @@ -0,0 +1,53 @@ +package iso + +import ( + "bytes" + "context" + "github.com/hashicorp/packer/builder/vsphere/driver" + "github.com/hashicorp/packer/common/bootcommand" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "testing" +) + +func TestStepBootCommand_Run(t *testing.T) { + state := new(multistep.BasicStateBag) + state.Put("ui", &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + }) + state.Put("debug", false) + state.Put("vm", new(driver.VirtualMachine)) + + state.Put("http_port", 2222) + state.Put("http_ip", "0.0.0.0") + + step := &StepBootCommand{ + Config: &BootConfig{ + BootConfig: bootcommand.BootConfig{ + BootCommand: []string{ + " initrd=/install/initrd.gz", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "/install/vmlinuz", + " initrd=/install/initrd.gz", + " priority=critical", + " locale=en_US", + " file=/media/preseed_hardcoded_ip.cfg", + " netcfg/get_ipaddress=0.0.0.0", + " netcfg/get_gateway=0.0.0.0", + "", + }, + }, + }, + } + step.Run(context.TODO(), state) +} diff --git a/common/bootcommand/usb_driver.go b/common/bootcommand/usb_driver.go new file mode 100644 index 000000000..7d0d34386 --- /dev/null +++ b/common/bootcommand/usb_driver.go @@ -0,0 +1,186 @@ +package bootcommand + +import ( + "fmt" + "github.com/hashicorp/packer/builder/vsphere/driver" + "github.com/hashicorp/packer/common" + "golang.org/x/mobile/event/key" + "log" + "os" + "strings" + "time" + "unicode" +) + +// SendUsbScanCodes will be called to send codes to the VM +type SendUsbScanCodes func([]key.Code, []bool) error + +type usbDriver struct { + vm *driver.VirtualMachine + + sendImpl SendUsbScanCodes + interval time.Duration + specialMap map[string]key.Code + scancodeMap map[rune]key.Code + + codeBuffer []key.Code + downBuffer []bool + + // keyEvent can set this error which will prevent it from continuing + err error +} + +func NewUSBDriver(send SendUsbScanCodes, interval time.Duration) *usbDriver { + // We delay (default 100ms) between each key event to allow for CPU or + // network latency. See PackerKeyEnv for tuning. + keyInterval := common.PackerKeyDefault + if delay, err := time.ParseDuration(os.Getenv(common.PackerKeyEnv)); err == nil { + keyInterval = delay + } + // override interval based on builder-specific override. + if interval > time.Duration(0) { + keyInterval = interval + } + + special := map[string]key.Code{ + "enter": key.CodeReturnEnter, + "esc": key.CodeEscape, + "bs": key.CodeDeleteBackspace, + "del": key.CodeDeleteForward, + "tab": key.CodeTab, + "f1": key.CodeF1, + "f2": key.CodeF2, + "f3": key.CodeF3, + "f4": key.CodeF4, + "f5": key.CodeF5, + "f6": key.CodeF6, + "f7": key.CodeF7, + "f8": key.CodeF8, + "f9": key.CodeF9, + "f10": key.CodeF10, + "f11": key.CodeF11, + "f12": key.CodeF12, + "insert": key.CodeInsert, + "home": key.CodeHome, + "end": key.CodeEnd, + "pageUp": key.CodePageUp, + "pageDown": key.CodePageDown, + "left": key.CodeLeftArrow, + "right": key.CodeRightArrow, + "up": key.CodeUpArrow, + "down": key.CodeDownArrow, + "leftalt": key.CodeLeftAlt, + "leftctrl": key.CodeLeftControl, + "leftshift": key.CodeLeftShift, + "rightalt": key.CodeRightAlt, + "rightctrl": key.CodeRightControl, + "rightshift": key.CodeRightShift, + } + + scancodeIndex := make(map[string]key.Code) + scancodeIndex["abcdefghijklmnopqrstuvwxyz"] = key.CodeA + scancodeIndex["ABCDEFGHIJKLMNOPQRSTUVWXYZ"] = key.CodeA + scancodeIndex["1234567890"] = key.Code1 + scancodeIndex["!@#$%^&*()"] = key.Code1 + scancodeIndex[" "] = key.CodeSpacebar + scancodeIndex["-=[]\\"] = key.CodeHyphenMinus + scancodeIndex["_+{}|"] = key.CodeHyphenMinus + scancodeIndex[";'`,./"] = key.CodeSemicolon + scancodeIndex[":\"~<>?"] = key.CodeSemicolon + + var scancodeMap = make(map[rune]key.Code) + for chars, start := range scancodeIndex { + for i, r := range chars { + scancodeMap[r] = start + key.Code(i) + } + } + + return &usbDriver{ + sendImpl: send, + specialMap: special, + interval: keyInterval, + scancodeMap: scancodeMap, + } +} + +//func (d *usbDriver) keyEvent(k key.Code, down bool) error { +// if d.err != nil { +// return nil +// } +// if err := d.sendImpl(k, down); err != nil { +// d.err = err +// return err +// } +// //time.Sleep(d.interval) +// return nil +//} + +// Flush does nothing here +func (d *usbDriver) Flush() error { + defer func() { + d.codeBuffer = nil + }() + + if err := d.sendImpl(d.codeBuffer, d.downBuffer); err != nil { + return err + } + time.Sleep(d.interval) + return nil +} + +func (d *usbDriver) SendKey(k rune, action KeyAction) error { + keyShift := unicode.IsUpper(k) || strings.ContainsRune(shiftedChars, k) + keyCode := d.scancodeMap[k] + log.Printf("Sending char '%c', code %s, shift %v", k, keyCode, keyShift) + + switch action { + case KeyOn: + if keyShift { + d.send(key.CodeLeftShift, true) + } + d.send(keyCode, true) + case KeyOff: + if keyShift { + d.send(key.CodeLeftShift, false) + } + d.send(keyCode, false) + case KeyPress: + if keyShift { + d.send(key.CodeLeftShift, true) + } + d.send(keyCode, true) + d.send(keyCode, false) + if keyShift { + d.send(key.CodeLeftShift, false) + } + } + return d.err +} + +func (d *usbDriver) SendSpecial(special string, action KeyAction) error { + keyCode, ok := d.specialMap[special] + if !ok { + return fmt.Errorf("special %s not found.", special) + } + log.Printf("Special code '<%s>' found, replacing with: %s", special, keyCode) + + switch action { + case KeyOn: + d.send(keyCode, true) + case KeyOff: + d.send(keyCode, false) + case KeyPress: + d.send(keyCode, true) + d.send(keyCode, false) + } + + return d.err +} + +// send stores the codes in an internal buffer. Use Flush to send them. +func (d *usbDriver) send(code key.Code, down bool) { + // slices to keep the input order + d.codeBuffer = append(d.codeBuffer, code) + d.downBuffer = append(d.downBuffer, down) + +}