From b636eb2bf18f525de540ec38684090a8a99fa2f7 Mon Sep 17 00:00:00 2001 From: Michael Kuzmin Date: Sun, 18 Feb 2018 05:13:56 +0300 Subject: [PATCH] Boot command (#53) --- driver/vm_keyboard.go | 82 ++++++++++++++ examples/ubuntu/preseed.cfg | 16 +++ examples/ubuntu/ubuntu-16.04.json | 63 +++++++++++ glide.lock | 9 +- glide.yaml | 1 + iso/builder.go | 3 + iso/config.go | 1 + iso/step_boot_command.go | 173 ++++++++++++++++++++++++++++++ 8 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 driver/vm_keyboard.go create mode 100644 examples/ubuntu/preseed.cfg create mode 100644 examples/ubuntu/ubuntu-16.04.json create mode 100644 iso/step_boot_command.go diff --git a/driver/vm_keyboard.go b/driver/vm_keyboard.go new file mode 100644 index 000000000..02b86a197 --- /dev/null +++ b/driver/vm_keyboard.go @@ -0,0 +1,82 @@ +package driver + +import ( + "strings" + "unicode" + "github.com/vmware/govmomi/vim25/types" + "github.com/vmware/govmomi/vim25/methods" + "golang.org/x/mobile/event/key" +) + +type KeyInput struct { + Message string + Scancode key.Code + Alt bool + Ctrl bool + 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, + }, + }) + } + + req := &types.PutUsbScanCodes{ + This: vm.vm.Reference(), + Spec: spec, + } + + resp, err := methods.PutUsbScanCodes(vm.driver.ctx, vm.driver.client.RoundTripper, req) + if err != nil { + return 0, err + } + + return resp.Returnval, nil +} diff --git a/examples/ubuntu/preseed.cfg b/examples/ubuntu/preseed.cfg new file mode 100644 index 000000000..ec963b6b2 --- /dev/null +++ b/examples/ubuntu/preseed.cfg @@ -0,0 +1,16 @@ +d-i passwd/user-fullname string jetbrains +d-i passwd/username string jetbrains +d-i passwd/user-password password jetbrains +d-i passwd/user-password-again password jetbrains +d-i user-setup/allow-password-weak boolean true + +d-i partman-auto/disk string /dev/sda +d-i partman-auto/method string regular +d-i partman-partitioning/confirm_write_new_label boolean true +d-i partman/choose_partition select finish +d-i partman/confirm boolean true +d-i partman/confirm_nooverwrite boolean true + +d-i pkgsel/include string open-vm-tools openssh-server + +d-i finish-install/reboot_in_progress note diff --git a/examples/ubuntu/ubuntu-16.04.json b/examples/ubuntu/ubuntu-16.04.json new file mode 100644 index 000000000..00d3d8227 --- /dev/null +++ b/examples/ubuntu/ubuntu-16.04.json @@ -0,0 +1,63 @@ +{ + "builders": [ + { + "type": "vsphere-iso", + + "vcenter_server": "vcenter.vsphere65.test", + "username": "root", + "password": "jetbrains", + "insecure_connection": "true", + + "vm_name": "example-ubuntu", + "host": "esxi-1.vsphere65.test", + + "guest_os_type": "ubuntu64Guest", + + "ssh_username": "jetbrains", + "ssh_password": "jetbrains", + + "CPUs": 1, + "RAM": 1024, + "RAM_reserve_all": true, + + "disk_controller_type": "pvscsi", + "disk_size": 32, + "disk_thin_provisioned": true, + + "network_card": "vmxnet3", + + "iso_paths": [ + "[datastore1] ISO/ubuntu-16.04.3-server-amd64.iso" + ], + "floppy_files": [ + "{{template_dir}}/preseed.cfg" + ], + "boot_command": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "/install/vmlinuz", + " initrd=/install/initrd.gz", + " priority=critical", + " locale=en_US", + " file=/media/preseed.cfg", + "" + ], + "boot_order": "disk,cdrom" + } + ], + + "provisioners": [ + { + "type": "shell", + "inline": ["ls /"] + } + ] +} diff --git a/glide.lock b/glide.lock index 5354d3cd5..8d0be2549 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 709325293541fff31ae48288c6f5873f67f90ae6ab69d9ae4465fcdf0626d843 -updated: 2017-06-27T10:42:45.867528732+03:00 +hash: 9dff375b5720b1ffb1164694bb6aba1f546c9e2a93509000eb9919e5241610f1 +updated: 2018-02-17T22:26:59.87828+03:00 imports: - name: github.com/Azure/go-ntlmssp version: 29affced641074a59483ed003b5ef73a8bd3593c @@ -26,6 +26,7 @@ imports: - communicator/none - communicator/ssh - communicator/winrm + - helper/builder/testing - helper/communicator - helper/config - packer @@ -105,4 +106,8 @@ imports: - md4 - ssh - ssh/agent +- name: golang.org/x/mobile + version: 4bfed40defcb2fed54d60707c13cec9d7888a483 + subpackages: + - event/key testImports: [] diff --git a/glide.yaml b/glide.yaml index a95b811d0..e84e824fd 100644 --- a/glide.yaml +++ b/glide.yaml @@ -6,3 +6,4 @@ import: - package: github.com/vmware/govmomi version: v0.15.0 - package: golang.org/x/crypto +- package: golang.org/x/mobile diff --git a/iso/builder.go b/iso/builder.go index 2e9307190..822506149 100644 --- a/iso/builder.go +++ b/iso/builder.go @@ -57,6 +57,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &common.StepRun{ Config: &b.config.RunConfig, }, + &StepBootCommand{ + Config: &b.config.BootConfig, + }, &common.StepWaitForIp{}, &communicator.StepConnect{ Config: &b.config.Comm, diff --git a/iso/config.go b/iso/config.go index 2b29c67ba..516a6d279 100644 --- a/iso/config.go +++ b/iso/config.go @@ -12,6 +12,7 @@ import ( type Config struct { packerCommon.PackerConfig `mapstructure:",squash"` common.RunConfig `mapstructure:",squash"` + BootConfig `mapstructure:",squash"` common.ConnectConfig `mapstructure:",squash"` Comm communicator.Config `mapstructure:",squash"` common.ShutdownConfig `mapstructure:",squash"` diff --git a/iso/step_boot_command.go b/iso/step_boot_command.go new file mode 100644 index 000000000..715a8d94d --- /dev/null +++ b/iso/step_boot_command.go @@ -0,0 +1,173 @@ +package iso + +import ( + "github.com/hashicorp/packer/packer" + "github.com/jetbrains-infra/packer-builder-vsphere/driver" + "github.com/mitchellh/multistep" + "fmt" + "time" + "strings" + "golang.org/x/mobile/event/key" + "unicode/utf8" + "github.com/hashicorp/packer/common" + "os" + "log" +) + +type BootConfig struct { + BootCommand []string `mapstructure:"boot_command"` +} + +func (c *BootConfig) Prepare() []error { + return nil +} + +type StepBootCommand struct { + Config *BootConfig +} + +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 = common.PackerKeyDefault + +func init() { + if delay, err := time.ParseDuration(os.Getenv(common.PackerKeyEnv)); err == nil { + keyInterval = delay + } +} + +func (s *StepBootCommand) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + vm := state.Get("vm").(*driver.VirtualMachine) + + ui.Say("Typing boot command...") + + var keyAlt bool + var keyCtrl bool + var keyShift bool + + for _, message := range s.Config.BootCommand { + for len(message) > 0 { + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return multistep.ActionHalt + } + + 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, + }) + if err != nil { + state.Put("error", fmt.Errorf("error typing a boot command: %v", err)) + return multistep.ActionHalt + } + time.Sleep(keyInterval) + } + } + + return multistep.ActionContinue +} + +func (s *StepBootCommand) Cleanup(state multistep.StateBag) {}