Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add boot steps configuration option #103

Merged
merged 8 commits into from
Nov 9, 2022
1 change: 1 addition & 0 deletions builder/qemu/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook)
QMPSocketPath: b.config.QMPSocketPath,
},
&stepTypeBootCommand{},
&stepTypeBootSteps{},
jacob-carlborg marked this conversation as resolved.
Show resolved Hide resolved
&stepWaitGuestAddress{
CommunicatorType: b.config.CommConfig.Comm.Type,
NetBridge: b.config.NetBridge,
Expand Down
39 changes: 39 additions & 0 deletions builder/qemu/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,45 @@ type Config struct {
// * ARM: tpm-tis-device
// * PPC (p-series): tpm-spapr
TPMType string `mapstructure:"tpm_device_type" required:"false"`
// This is an array of tuples of boot commands, to type when the virtual
// machine is booted. The first element of the tuple is the actual boot
// command. The second element of the tuple is a description of what the boot
// command does. This is intended to be used for interactive installers that
// requires many commands to complete the installation. Both the command and
// the description will be printed when logging is enabled. When debug mode is
// enabled Packer will pause after typing each boot command. This will make it
// easier to follow along the installation process and make sure the Packer
// and the installer are in sync. It's recommended to use either `boot_steps`
// or `boot_commands`.
jacob-carlborg marked this conversation as resolved.
Show resolved Hide resolved
//
// Example:
//
// In HCL:
// ```hcl
// boot_steps = [
// ["1<enter><wait5>", "Install NetBSD"],
// ["a<enter><wait5>", "Installation messages in English"],
// ["a<enter><wait5>", "Keyboard type: unchanged"],
//
// ["a<enter><wait5>", "Install NetBSD to hard disk"],
// ["b<enter><wait5>", "Yes"]
// ]
// ```
//
// In JSON:
// ```json
// {
// "boot_steps": [
// ["1<enter><wait5>", "Install NetBSD"],
// ["a<enter><wait5>", "Installation messages in English"],
// ["a<enter><wait5>", "Keyboard type: unchanged"],
//
// ["a<enter><wait5>", "Install NetBSD to hard disk"],
// ["b<enter><wait5>", "Yes"]
// ]
// }
jacob-carlborg marked this conversation as resolved.
Show resolved Hide resolved
BootSteps [][]string `mapstructure:"boot_steps" required:"false"`

// TODO(mitchellh): deprecate
RunOnce bool `mapstructure:"run_once"`

Expand Down
2 changes: 2 additions & 0 deletions builder/qemu/config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 3 additions & 105 deletions builder/qemu/step_type_boot_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@ package qemu

import (
"context"
"fmt"
"log"
"net"
"time"

"github.com/hashicorp/packer-plugin-sdk/bootcommand"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/mitchellh/go-vnc"
)

const KeyLeftShift uint32 = 0xFFE1
Expand All @@ -36,104 +28,10 @@ type stepTypeBootCommand struct{}

func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
debug := state.Get("debug").(bool)
httpPort := state.Get("http_port").(int)
ui := state.Get("ui").(packersdk.Ui)
vncPort := state.Get("vnc_port").(int)
vncIP := config.VNCBindAddress
vncPassword := state.Get("vnc_password")
command := config.VNCConfig.FlatBootCommand()
bootSteps := [][]string{{command}}

if config.VNCConfig.DisableVNC {
log.Println("Skipping boot command step...")
return multistep.ActionContinue
}

// Wait the for the vm to boot.
if int64(config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.BootWait))
select {
case <-time.After(config.BootWait):
break
case <-ctx.Done():
return multistep.ActionHalt
}
}

var pauseFn multistep.DebugPauseFn
if debug {
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
}

// Connect to VNC
ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIP, vncPort))

nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIP, vncPort))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer nc.Close()

var auth []vnc.ClientAuth

if vncPassword != nil && len(vncPassword.(string)) > 0 {
auth = []vnc.ClientAuth{&vnc.PasswordAuth{Password: vncPassword.(string)}}
} else {
auth = []vnc.ClientAuth{new(vnc.ClientAuthNone)}
}

c, err := vnc.Client(nc, &vnc.ClientConfig{Auth: auth, Exclusive: false})
if err != nil {
err := fmt.Errorf("Error handshaking with VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer c.Close()

log.Printf("Connected to VNC desktop: %s", c.DesktopName)

hostIP := state.Get("http_ip").(string)
configCtx := config.ctx
configCtx.Data = &bootCommandTemplateData{
hostIP,
httpPort,
config.VMName,
}

d := bootcommand.NewVNCDriver(c, config.VNCConfig.BootKeyInterval)

ui.Say("Typing the boot command over VNC...")
command, err := interpolate.Render(config.VNCConfig.FlatBootCommand(), &configCtx)
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
return typeBootCommands(ctx, state, bootSteps)
}

func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
163 changes: 163 additions & 0 deletions builder/qemu/step_type_boot_steps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package qemu

import (
"context"
"fmt"
"log"
"net"
"time"

"github.com/hashicorp/packer-plugin-sdk/bootcommand"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/mitchellh/go-vnc"
)

// This step "types" the boot command into the VM over VNC.
//
// Uses:
// config *config
// http_port int
// ui packersdk.Ui
// vnc_port int
//
// Produces:
// <nothing>
type stepTypeBootSteps struct{}

func typeBootCommands(ctx context.Context, state multistep.StateBag, bootSteps [][]string) multistep.StepAction {
config := state.Get("config").(*Config)
debug := state.Get("debug").(bool)
httpPort := state.Get("http_port").(int)
ui := state.Get("ui").(packersdk.Ui)
vncPort := state.Get("vnc_port").(int)
vncIP := config.VNCBindAddress
vncPassword := state.Get("vnc_password")

if config.VNCConfig.DisableVNC {
log.Println("Skipping boot command step...")
return multistep.ActionContinue
}

// Wait the for the vm to boot.
if int64(config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.BootWait))
select {
case <-time.After(config.BootWait):
break
case <-ctx.Done():
return multistep.ActionHalt
}
}

var pauseFn multistep.DebugPauseFn
if debug {
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
}

// Connect to VNC
ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIP, vncPort))

nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIP, vncPort))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer nc.Close()

var auth []vnc.ClientAuth

if vncPassword != nil && len(vncPassword.(string)) > 0 {
auth = []vnc.ClientAuth{&vnc.PasswordAuth{Password: vncPassword.(string)}}
} else {
auth = []vnc.ClientAuth{new(vnc.ClientAuthNone)}
}

c, err := vnc.Client(nc, &vnc.ClientConfig{Auth: auth, Exclusive: false})
if err != nil {
err := fmt.Errorf("Error handshaking with VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer c.Close()

log.Printf("Connected to VNC desktop: %s", c.DesktopName)

hostIP := state.Get("http_ip").(string)
configCtx := config.ctx
configCtx.Data = &bootCommandTemplateData{
hostIP,
httpPort,
config.VMName,
}

d := bootcommand.NewVNCDriver(c, config.VNCConfig.BootKeyInterval)

ui.Say("Typing the boot commands over VNC...")

for _, step := range bootSteps {
if len(step) == 0 {
continue
}

var description string

if len(step) >= 2 {
description = step[1]
} else {
description = ""
}

if len(description) > 0 {
log.Printf("Typing boot command for: %s", description)
}

command, err := interpolate.Render(step[0], &configCtx)

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 {
var message string

if len(description) > 0 {
message = fmt.Sprintf("boot description: \"%s\", command: %s", description, command)
} else {
message = fmt.Sprintf("boot_command: %s", command)
}

pauseFn(multistep.DebugLocationAfterRun, message, state)
}
}

return multistep.ActionContinue
}

func (s *stepTypeBootSteps) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
return typeBootCommands(ctx, state, state.Get("config").(*Config).BootSteps)
}

func (*stepTypeBootSteps) Cleanup(multistep.StateBag) {}
Loading