diff --git a/builder/qemu/config.go b/builder/qemu/config.go index a0093e7..7935f13 100644 --- a/builder/qemu/config.go +++ b/builder/qemu/config.go @@ -520,6 +520,46 @@ 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"` @@ -758,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("boot_command and boot_steps cannot be used together")) + } + if c.NetBridge != "" || c.VNCUsePassword { c.QMPEnable = true } diff --git a/builder/qemu/config.hcl2spec.go b/builder/qemu/config.hcl2spec.go index 1eb962c..5135595 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 038f18d..4af9682 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -35,6 +35,20 @@ type bootCommandTemplateData struct { type stepTypeBootCommand struct{} func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + command := config.VNCConfig.FlatBootCommand() + bootSteps := config.BootSteps + + 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) @@ -105,35 +119,61 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) 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 - } + ui.Say("Typing the boot commands over VNC...") - 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 - } + for _, step := range bootSteps { + if len(step) == 0 { + continue + } - 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 - } + var description string + + if len(step) >= 2 { + description = step[1] + } else { + description = "" + } + + if len(description) > 0 { + ui.Say(fmt.Sprintf("Typing boot command for: %s", description)) + } - if pauseFn != nil { - pauseFn(multistep.DebugLocationAfterRun, fmt.Sprintf("boot_command: %s", command), state) + 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 (*stepTypeBootCommand) Cleanup(multistep.StateBag) {} diff --git a/docs-partials/builder/qemu/Config-not-required.mdx b/docs-partials/builder/qemu/Config-not-required.mdx index ef5797d..57207f4 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"] + ] + } + ``` +