From 628c457f82e6ffcc26b6b9da0fe1e7ef1ea288a5 Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Sat, 8 Oct 2022 21:05:01 +0200 Subject: [PATCH 1/8] Add boot steps configuration option Adds a new configuration option called `boot_steps`. 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. Closes hashicorp/packer#11824 Closes hashicorp/packer#11286 --- builder/qemu/builder.go | 1 + builder/qemu/config.go | 39 +++++ builder/qemu/config.hcl2spec.go | 2 + builder/qemu/step_type_boot_command.go | 108 +----------- builder/qemu/step_type_boot_steps.go | 163 ++++++++++++++++++ .../builder/qemu/Config-not-required.mdx | 38 ++++ 6 files changed, 246 insertions(+), 105 deletions(-) create mode 100644 builder/qemu/step_type_boot_steps.go diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index d308c3e7..837111b8 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -118,6 +118,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) QMPSocketPath: b.config.QMPSocketPath, }, &stepTypeBootCommand{}, + &stepTypeBootSteps{}, &stepWaitGuestAddress{ CommunicatorType: b.config.CommConfig.Comm.Type, NetBridge: b.config.NetBridge, diff --git a/builder/qemu/config.go b/builder/qemu/config.go index a0093e71..27193879 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -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`. + // + // Example: + // + // In HCL: + // ```hcl + // boot_steps = [ + // ["1", "Install NetBSD"], + // ["a", "Installation messages in English"], + // ["a", "Keyboard type: unchanged"], + // + // ["a", "Install NetBSD to hard disk"], + // ["b", "Yes"] + // ] + // ``` + // + // In JSON: + // ```json + // { + // "boot_steps": [ + // ["1", "Install NetBSD"], + // ["a", "Installation messages in English"], + // ["a", "Keyboard type: unchanged"], + // + // ["a", "Install NetBSD to hard disk"], + // ["b", "Yes"] + // ] + // } + BootSteps [][]string `mapstructure:"boot_steps" required:"false"` + // TODO(mitchellh): deprecate RunOnce bool `mapstructure:"run_once"` diff --git a/builder/qemu/config.hcl2spec.go b/builder/qemu/config.hcl2spec.go index 1eb962c4..5135595c 100644 --- a/builder/qemu/config.hcl2spec.go +++ b/builder/qemu/config.hcl2spec.go @@ -139,6 +139,7 @@ type FlatConfig struct { VTPM *bool `mapstructure:"vtpm" required:"false" cty:"vtpm" hcl:"vtpm"` VTPMUseTPM1 *bool `mapstructure:"use_tpm1" required:"false" cty:"use_tpm1" hcl:"use_tpm1"` TPMType *string `mapstructure:"tpm_device_type" required:"false" cty:"tpm_device_type" hcl:"tpm_device_type"` + BootSteps [][]string `mapstructure:"boot_steps" required:"false" cty:"boot_steps" hcl:"boot_steps"` RunOnce *bool `mapstructure:"run_once" cty:"run_once" hcl:"run_once"` } @@ -283,6 +284,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "vtpm": &hcldec.AttrSpec{Name: "vtpm", Type: cty.Bool, Required: false}, "use_tpm1": &hcldec.AttrSpec{Name: "use_tpm1", Type: cty.Bool, Required: false}, "tpm_device_type": &hcldec.AttrSpec{Name: "tpm_device_type", Type: cty.String, Required: false}, + "boot_steps": &hcldec.AttrSpec{Name: "boot_steps", Type: cty.List(cty.List(cty.String)), Required: false}, "run_once": &hcldec.AttrSpec{Name: "run_once", Type: cty.Bool, Required: false}, } return s diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go index 038f18d6..e3121b39 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -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 @@ -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) {} diff --git a/builder/qemu/step_type_boot_steps.go b/builder/qemu/step_type_boot_steps.go new file mode 100644 index 00000000..1086a6f1 --- /dev/null +++ b/builder/qemu/step_type_boot_steps.go @@ -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: +// +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) {} diff --git a/docs-partials/builder/qemu/Config-not-required.mdx b/docs-partials/builder/qemu/Config-not-required.mdx index ef5797d8..0556e75f 100644 --- a/docs-partials/builder/qemu/Config-not-required.mdx +++ b/docs-partials/builder/qemu/Config-not-required.mdx @@ -350,4 +350,42 @@ * ARM: tpm-tis-device * PPC (p-series): tpm-spapr +- `boot_steps` ([][]string) - 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`. + + Example: + + In HCL: + ```hcl + boot_steps = [ + ["1", "Install NetBSD"], + ["a", "Installation messages in English"], + ["a", "Keyboard type: unchanged"], + + ["a", "Install NetBSD to hard disk"], + ["b", "Yes"] + ] + ``` + + In JSON: + ```json + { + "boot_steps": [ + ["1", "Install NetBSD"], + ["a", "Installation messages in English"], + ["a", "Keyboard type: unchanged"], + + ["a", "Install NetBSD to hard disk"], + ["b", "Yes"] + ] + } + From 33162fd078beab403b47b3a57633b2cf830d595c Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Mon, 24 Oct 2022 17:29:59 +0200 Subject: [PATCH 2/8] fixup! Add boot steps configuration option --- builder/qemu/config.go | 1 + docs-partials/builder/qemu/Config-not-required.mdx | 1 + 2 files changed, 2 insertions(+) diff --git a/builder/qemu/config.go b/builder/qemu/config.go index 27193879..81a396bf 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -557,6 +557,7 @@ type Config struct { // ["b", "Yes"] // ] // } + // ``` BootSteps [][]string `mapstructure:"boot_steps" required:"false"` // TODO(mitchellh): deprecate diff --git a/docs-partials/builder/qemu/Config-not-required.mdx b/docs-partials/builder/qemu/Config-not-required.mdx index 0556e75f..57820b8b 100644 --- a/docs-partials/builder/qemu/Config-not-required.mdx +++ b/docs-partials/builder/qemu/Config-not-required.mdx @@ -387,5 +387,6 @@ ["b", "Yes"] ] } + ``` From 867fdfcd220f4d89808c73e36dbce03e9fcefb0a Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Wed, 26 Oct 2022 20:57:32 +0200 Subject: [PATCH 3/8] fixup! Add boot steps configuration option --- builder/qemu/builder.go | 1 - builder/qemu/step_type_boot_command.go | 151 ++++++++++++++++++++++- builder/qemu/step_type_boot_steps.go | 163 ------------------------- 3 files changed, 150 insertions(+), 165 deletions(-) delete mode 100644 builder/qemu/step_type_boot_steps.go diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index 837111b8..d308c3e7 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -118,7 +118,6 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) QMPSocketPath: b.config.QMPSocketPath, }, &stepTypeBootCommand{}, - &stepTypeBootSteps{}, &stepWaitGuestAddress{ CommunicatorType: b.config.CommConfig.Comm.Type, NetBridge: b.config.NetBridge, diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go index e3121b39..e459f4dc 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -2,8 +2,16 @@ 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 @@ -27,11 +35,152 @@ type bootCommandTemplateData struct { type stepTypeBootCommand struct{} func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packersdk.Ui) config := state.Get("config").(*Config) command := config.VNCConfig.FlatBootCommand() - bootSteps := [][]string{{command}} + bootSteps := config.BootSteps + + if len(command) > 0 && len(bootSteps) > 0 { + err := fmt.Errorf("Both boot command and boot steps cannot be used.") + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if len(command) > 0 { + bootSteps = [][]string{{command}} + } return typeBootCommands(ctx, state, bootSteps) } func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} + +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 +} diff --git a/builder/qemu/step_type_boot_steps.go b/builder/qemu/step_type_boot_steps.go deleted file mode 100644 index 1086a6f1..00000000 --- a/builder/qemu/step_type_boot_steps.go +++ /dev/null @@ -1,163 +0,0 @@ -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: -// -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) {} From 64aee894bae05679178443b73daed6917010acc9 Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Wed, 26 Oct 2022 21:04:26 +0200 Subject: [PATCH 4/8] fixup! Add boot steps configuration option --- builder/qemu/config.go | 16 ++++++++-------- .../builder/qemu/Config-not-required.mdx | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/builder/qemu/config.go b/builder/qemu/config.go index 81a396bf..b7fa26b6 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -522,14 +522,14 @@ type Config struct { 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`. + // command. The second element of the tuple, which is optional, 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. `boot_steps` and `boot_commands` are mutually exclusive. // // Example: // diff --git a/docs-partials/builder/qemu/Config-not-required.mdx b/docs-partials/builder/qemu/Config-not-required.mdx index 57820b8b..57207f4f 100644 --- a/docs-partials/builder/qemu/Config-not-required.mdx +++ b/docs-partials/builder/qemu/Config-not-required.mdx @@ -352,14 +352,14 @@ - `boot_steps` ([][]string) - 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`. + command. The second element of the tuple, which is optional, 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. `boot_steps` and `boot_commands` are mutually exclusive. Example: From e00cc1923144ef97b2dfe2146c69b30e6ccbc934 Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Fri, 28 Oct 2022 20:38:53 +0200 Subject: [PATCH 5/8] fixup! Add boot steps configuration option --- builder/qemu/config.go | 39 ------------------- builder/qemu/config.hcl2spec.go | 4 +- builder/qemu/step_type_boot_command.go | 7 ---- .../builder/qemu/Config-not-required.mdx | 39 ------------------- 4 files changed, 2 insertions(+), 87 deletions(-) diff --git a/builder/qemu/config.go b/builder/qemu/config.go index b7fa26b6..b7161234 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -520,45 +520,6 @@ 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, which is optional, 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. `boot_steps` and `boot_commands` are mutually exclusive. - // - // Example: - // - // In HCL: - // ```hcl - // boot_steps = [ - // ["1", "Install NetBSD"], - // ["a", "Installation messages in English"], - // ["a", "Keyboard type: unchanged"], - // - // ["a", "Install NetBSD to hard disk"], - // ["b", "Yes"] - // ] - // ``` - // - // In JSON: - // ```json - // { - // "boot_steps": [ - // ["1", "Install NetBSD"], - // ["a", "Installation messages in English"], - // ["a", "Keyboard type: unchanged"], - // - // ["a", "Install NetBSD to hard disk"], - // ["b", "Yes"] - // ] - // } - // ``` - BootSteps [][]string `mapstructure:"boot_steps" required:"false"` // TODO(mitchellh): deprecate RunOnce bool `mapstructure:"run_once"` diff --git a/builder/qemu/config.hcl2spec.go b/builder/qemu/config.hcl2spec.go index 5135595c..9e630e3e 100644 --- a/builder/qemu/config.hcl2spec.go +++ b/builder/qemu/config.hcl2spec.go @@ -32,6 +32,7 @@ type FlatConfig struct { BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"` BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"` BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` + BootSteps [][]string `mapstructure:"boot_steps" required:"false" cty:"boot_steps" hcl:"boot_steps"` 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"` ShutdownCommand *string `mapstructure:"shutdown_command" required:"false" cty:"shutdown_command" hcl:"shutdown_command"` @@ -139,7 +140,6 @@ type FlatConfig struct { VTPM *bool `mapstructure:"vtpm" required:"false" cty:"vtpm" hcl:"vtpm"` VTPMUseTPM1 *bool `mapstructure:"use_tpm1" required:"false" cty:"use_tpm1" hcl:"use_tpm1"` TPMType *string `mapstructure:"tpm_device_type" required:"false" cty:"tpm_device_type" hcl:"tpm_device_type"` - BootSteps [][]string `mapstructure:"boot_steps" required:"false" cty:"boot_steps" hcl:"boot_steps"` RunOnce *bool `mapstructure:"run_once" cty:"run_once" hcl:"run_once"` } @@ -177,6 +177,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false}, "boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false}, "boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false}, + "boot_steps": &hcldec.AttrSpec{Name: "boot_steps", Type: cty.List(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}, "shutdown_command": &hcldec.AttrSpec{Name: "shutdown_command", Type: cty.String, Required: false}, @@ -284,7 +285,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "vtpm": &hcldec.AttrSpec{Name: "vtpm", Type: cty.Bool, Required: false}, "use_tpm1": &hcldec.AttrSpec{Name: "use_tpm1", Type: cty.Bool, Required: false}, "tpm_device_type": &hcldec.AttrSpec{Name: "tpm_device_type", Type: cty.String, Required: false}, - "boot_steps": &hcldec.AttrSpec{Name: "boot_steps", Type: cty.List(cty.List(cty.String)), Required: false}, "run_once": &hcldec.AttrSpec{Name: "run_once", Type: cty.Bool, Required: false}, } return s diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go index e459f4dc..510a5b68 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -35,17 +35,10 @@ type bootCommandTemplateData struct { type stepTypeBootCommand struct{} func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - ui := state.Get("ui").(packersdk.Ui) config := state.Get("config").(*Config) command := config.VNCConfig.FlatBootCommand() bootSteps := config.BootSteps - if len(command) > 0 && len(bootSteps) > 0 { - err := fmt.Errorf("Both boot command and boot steps cannot be used.") - ui.Error(err.Error()) - return multistep.ActionHalt - } - if len(command) > 0 { bootSteps = [][]string{{command}} } diff --git a/docs-partials/builder/qemu/Config-not-required.mdx b/docs-partials/builder/qemu/Config-not-required.mdx index 57207f4f..ef5797d8 100644 --- a/docs-partials/builder/qemu/Config-not-required.mdx +++ b/docs-partials/builder/qemu/Config-not-required.mdx @@ -350,43 +350,4 @@ * ARM: tpm-tis-device * PPC (p-series): tpm-spapr -- `boot_steps` ([][]string) - 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, which is optional, 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. `boot_steps` and `boot_commands` are mutually exclusive. - - Example: - - In HCL: - ```hcl - boot_steps = [ - ["1", "Install NetBSD"], - ["a", "Installation messages in English"], - ["a", "Keyboard type: unchanged"], - - ["a", "Install NetBSD to hard disk"], - ["b", "Yes"] - ] - ``` - - In JSON: - ```json - { - "boot_steps": [ - ["1", "Install NetBSD"], - ["a", "Installation messages in English"], - ["a", "Keyboard type: unchanged"], - - ["a", "Install NetBSD to hard disk"], - ["b", "Yes"] - ] - } - ``` - From 4de68fb0ead839d25a695a216eda330f0001fb56 Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Mon, 7 Nov 2022 20:38:15 +0100 Subject: [PATCH 6/8] fixup! Add boot steps configuration option --- builder/qemu/config.go | 44 +++++++++++++++++++ builder/qemu/config.hcl2spec.go | 4 +- .../builder/qemu/Config-not-required.mdx | 39 ++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/builder/qemu/config.go b/builder/qemu/config.go index b7161234..b7d1dcab 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -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, which is optional, 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. `boot_steps` and `boot_commands` are mutually exclusive. + // + // Example: + // + // In HCL: + // ```hcl + // boot_steps = [ + // ["1", "Install NetBSD"], + // ["a", "Installation messages in English"], + // ["a", "Keyboard type: unchanged"], + // + // ["a", "Install NetBSD to hard disk"], + // ["b", "Yes"] + // ] + // ``` + // + // In JSON: + // ```json + // { + // "boot_steps": [ + // ["1", "Install NetBSD"], + // ["a", "Installation messages in English"], + // ["a", "Keyboard type: unchanged"], + // + // ["a", "Install NetBSD to hard disk"], + // ["b", "Yes"] + // ] + // } + // ``` + BootSteps [][]string `mapstructure:"boot_steps" required:"false"` // TODO(mitchellh): deprecate RunOnce bool `mapstructure:"run_once"` @@ -759,6 +798,11 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs, fmt.Errorf("net_bridge is only supported in Linux based OSes")) } + if len(c.BootCommand) > 0 && len(c.BootSteps) > 0 { + errs = packersdk.MultiErrorAppend(errs, + fmt.Errorf("Both boot_command and boot_steps cannot be used")) + } + if c.NetBridge != "" || c.VNCUsePassword { c.QMPEnable = true } diff --git a/builder/qemu/config.hcl2spec.go b/builder/qemu/config.hcl2spec.go index 9e630e3e..5135595c 100644 --- a/builder/qemu/config.hcl2spec.go +++ b/builder/qemu/config.hcl2spec.go @@ -32,7 +32,6 @@ type FlatConfig struct { BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"` BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"` BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` - BootSteps [][]string `mapstructure:"boot_steps" required:"false" cty:"boot_steps" hcl:"boot_steps"` 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"` ShutdownCommand *string `mapstructure:"shutdown_command" required:"false" cty:"shutdown_command" hcl:"shutdown_command"` @@ -140,6 +139,7 @@ type FlatConfig struct { VTPM *bool `mapstructure:"vtpm" required:"false" cty:"vtpm" hcl:"vtpm"` VTPMUseTPM1 *bool `mapstructure:"use_tpm1" required:"false" cty:"use_tpm1" hcl:"use_tpm1"` TPMType *string `mapstructure:"tpm_device_type" required:"false" cty:"tpm_device_type" hcl:"tpm_device_type"` + BootSteps [][]string `mapstructure:"boot_steps" required:"false" cty:"boot_steps" hcl:"boot_steps"` RunOnce *bool `mapstructure:"run_once" cty:"run_once" hcl:"run_once"` } @@ -177,7 +177,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false}, "boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false}, "boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false}, - "boot_steps": &hcldec.AttrSpec{Name: "boot_steps", Type: cty.List(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}, "shutdown_command": &hcldec.AttrSpec{Name: "shutdown_command", Type: cty.String, Required: false}, @@ -285,6 +284,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "vtpm": &hcldec.AttrSpec{Name: "vtpm", Type: cty.Bool, Required: false}, "use_tpm1": &hcldec.AttrSpec{Name: "use_tpm1", Type: cty.Bool, Required: false}, "tpm_device_type": &hcldec.AttrSpec{Name: "tpm_device_type", Type: cty.String, Required: false}, + "boot_steps": &hcldec.AttrSpec{Name: "boot_steps", Type: cty.List(cty.List(cty.String)), Required: false}, "run_once": &hcldec.AttrSpec{Name: "run_once", Type: cty.Bool, Required: false}, } return s diff --git a/docs-partials/builder/qemu/Config-not-required.mdx b/docs-partials/builder/qemu/Config-not-required.mdx index ef5797d8..57207f4f 100644 --- a/docs-partials/builder/qemu/Config-not-required.mdx +++ b/docs-partials/builder/qemu/Config-not-required.mdx @@ -350,4 +350,43 @@ * ARM: tpm-tis-device * PPC (p-series): tpm-spapr +- `boot_steps` ([][]string) - 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, which is optional, 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. `boot_steps` and `boot_commands` are mutually exclusive. + + Example: + + In HCL: + ```hcl + boot_steps = [ + ["1", "Install NetBSD"], + ["a", "Installation messages in English"], + ["a", "Keyboard type: unchanged"], + + ["a", "Install NetBSD to hard disk"], + ["b", "Yes"] + ] + ``` + + In JSON: + ```json + { + "boot_steps": [ + ["1", "Install NetBSD"], + ["a", "Installation messages in English"], + ["a", "Keyboard type: unchanged"], + + ["a", "Install NetBSD to hard disk"], + ["b", "Yes"] + ] + } + ``` + From d107275f3b617e11134c2201bf0589215b58f202 Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Wed, 9 Nov 2022 11:41:57 +0100 Subject: [PATCH 7/8] fixup! Add boot steps configuration option --- builder/qemu/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/qemu/config.go b/builder/qemu/config.go index b7d1dcab..7935f133 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -800,7 +800,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { if len(c.BootCommand) > 0 && len(c.BootSteps) > 0 { errs = packersdk.MultiErrorAppend(errs, - fmt.Errorf("Both boot_command and boot_steps cannot be used")) + fmt.Errorf("boot_command and boot_steps cannot be used together")) } if c.NetBridge != "" || c.VNCUsePassword { From a62cb69e281ef0f1d0e6695e3b5e801cf8b544b8 Mon Sep 17 00:00:00 2001 From: Jacob Carlborg Date: Wed, 9 Nov 2022 11:48:13 +0100 Subject: [PATCH 8/8] fixup! Add boot steps configuration option --- builder/qemu/step_type_boot_command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go index 510a5b68..4af96821 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -135,7 +135,7 @@ func typeBootCommands(ctx context.Context, state multistep.StateBag, bootSteps [ } if len(description) > 0 { - log.Printf("Typing boot command for: %s", description) + ui.Say(fmt.Sprintf("Typing boot command for: %s", description)) } command, err := interpolate.Render(step[0], &configCtx)