Add usb_driver to common boot_command and use it on vsphere

This commit is contained in:
Moss 2020-06-11 18:37:32 +02:00
parent fcf7e634d9
commit 585a86fe03
7 changed files with 381 additions and 245 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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{
"<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,
}
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:
// <leftAltOn>
keyAlt = downs[i]
case key.CodeLeftControl:
// <leftCtrlOn>
keyCtrl = downs[i]
case key.CodeLeftShift:
// <leftShiftOn>
keyShift = downs[i]
}
if strings.HasPrefix(message, "<wait>") {
log.Printf("Waiting 1 second")
time.Sleep(1 * time.Second)
message = message[len("<wait>"):]
continue
}
if strings.HasPrefix(message, "<wait5>") {
log.Printf("Waiting 5 seconds")
time.Sleep(5 * time.Second)
message = message[len("<wait5>"):]
continue
}
if strings.HasPrefix(message, "<wait10>") {
log.Printf("Waiting 10 seconds")
time.Sleep(10 * time.Second)
message = message[len("<wait10>"):]
continue
}
if strings.HasPrefix(message, "<leftAltOn>") {
keyAlt = true
message = message[len("<leftAltOn>"):]
continue
}
if strings.HasPrefix(message, "<leftAltOff>") {
keyAlt = false
message = message[len("<leftAltOff>"):]
continue
}
if strings.HasPrefix(message, "<leftCtrlOn>") {
keyCtrl = true
message = message[len("<leftCtrlOn>"):]
continue
}
if strings.HasPrefix(message, "<leftCtrlOff>") {
keyCtrl = false
message = message[len("<leftCtrlOff>"):]
continue
}
if strings.HasPrefix(message, "<leftShiftOn>") {
keyShift = true
message = message[len("<leftShiftOn>"):]
continue
}
if strings.HasPrefix(message, "<leftShiftOff>") {
keyShift = false
message = message[len("<leftShiftOff>"):]
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) {}

View File

@ -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",
"<leftShiftOn><enter><wait><f6><wait><esc><wait>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs><bs><bs><bs><bs><bs><bs><bs>",
"<bs><bs><bs>",
"/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",
"<enter>",
},
},
},
}
step.Run(context.TODO(), state)
}

View File

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