diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index a9a021e35..500f246d6 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -126,6 +126,7 @@ type Config struct { VNCBindAddress string `mapstructure:"vnc_bind_address"` VNCPortMin int `mapstructure:"vnc_port_min"` VNCPortMax int `mapstructure:"vnc_port_max"` + VNCUsePassword bool `mapstructure:"vnc_use_password"` VMName string `mapstructure:"vm_name"` // These are deprecated, but we keep them around for BC @@ -357,6 +358,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) } + if b.config.VNCUsePassword && !b.config.QMPEnable { + b.config.QMPEnable = true + } + if b.config.QMPEnable && b.config.QMPSocketPath == "" { socketName := fmt.Sprintf("%s.monitor", b.config.VMName) b.config.QMPSocketPath = filepath.Join(b.config.OutputDir, socketName) diff --git a/builder/qemu/builder_test.go b/builder/qemu/builder_test.go index a064c1265..de1e1d7b2 100644 --- a/builder/qemu/builder_test.go +++ b/builder/qemu/builder_test.go @@ -113,6 +113,25 @@ func TestBuilderPrepare_VNCBindAddress(t *testing.T) { } } +func TestBuilderPrepare_VNCPassword(t *testing.T) { + var b Builder + config := testConfig() + + // Test a default boot_wait + config["vnc_use_password"] = true + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + if !b.config.QMPEnable { + t.Fatalf("QMP should be enabled.") + } +} + func TestBuilderPrepare_DiskCompaction(t *testing.T) { var b Builder config := testConfig() diff --git a/builder/qemu/step_configure_qmp.go b/builder/qemu/step_configure_qmp.go index 9afbb865e..957807a29 100644 --- a/builder/qemu/step_configure_qmp.go +++ b/builder/qemu/step_configure_qmp.go @@ -30,27 +30,63 @@ func (s *stepConfigureQMP) Run(ctx context.Context, state multistep.StateBag) mu return multistep.ActionContinue } - msg := fmt.Sprintf("Opening QMP socket at: %s", config.QMPSocketPath) + msg := fmt.Sprintf("QMP socket at: %s", config.QMPSocketPath) ui.Say(msg) log.Print(msg) - // Open QMP socket - var err error - s.monitor, err = qmp.NewSocketMonitor("unix", config.QMPSocketPath, 2*time.Second) - if err != nil { - err := fmt.Errorf("Error opening QMP socket: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt + // Only initialize and open QMP when we have a use for it. + // Handles cases where user may want the socket, but we don't + if config.VNCUsePassword { + // Open QMP socket + var err error + var cmd []byte + var result []byte + s.monitor, err = qmp.NewSocketMonitor("unix", config.QMPSocketPath, 2*time.Second) + if err != nil { + err := fmt.Errorf("Error opening QMP socket: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + QMPMonitor := s.monitor + vncPassword := state.Get("vnc_password") + + // Connect to QMP + // function automatically calls capabilities so is immediately ready for commands + err = QMPMonitor.Connect() + if err != nil { + err := fmt.Errorf("Error connecting to QMP socket: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + log.Printf("QMP socket open SUCCESS") + + cmd = []byte(fmt.Sprintf("{ \"execute\": \"change-vnc-password\", \"arguments\": { \"password\": \"%s\" } }", + vncPassword)) + result, err = QMPMonitor.Run(cmd) + if err != nil { + err := fmt.Errorf("Error connecting to QMP socket: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + msg = fmt.Sprintf("QMP Command: %s\nResult: %s", cmd, result) + log.Printf(msg) + + // Put QMP monitor in statebag in case there is a use in a following step + // Uncomment for future case as it is unused for now + //state.Put("qmp_monitor", QMPMonitor) } - QMPMonitor := s.monitor - - log.Printf("QMP socket open SUCCESS") - - state.Put("qmp_monitor", QMPMonitor) return multistep.ActionContinue } func (s *stepConfigureQMP) Cleanup(multistep.StateBag) { + if s.monitor != nil { + err := s.monitor.Disconnect() + if err != nil { + log.Printf("failed to disconnect QMP: %v", err) + } + } } diff --git a/builder/qemu/step_configure_vnc.go b/builder/qemu/step_configure_vnc.go index 465288e7f..34aed791e 100644 --- a/builder/qemu/step_configure_vnc.go +++ b/builder/qemu/step_configure_vnc.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "math/rand" "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/multistep" @@ -22,6 +23,21 @@ type stepConfigureVNC struct { l *net.Listener } +func VNCPassword() string { + length := int(8) + + charSet := []byte("012345689abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + charSetLength := len(charSet) + + password := make([]byte, length) + + for i := 0; i < length; i++ { + password[i] = charSet[rand.Intn(charSetLength)] + } + + return string(password) +} + func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) ui := state.Get("ui").(packer.Ui) @@ -33,6 +49,7 @@ func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) mu ui.Say(msg) log.Print(msg) + var vncPassword string var err error s.l, err = net.ListenRangeConfig{ Addr: config.VNCBindAddress, @@ -41,7 +58,7 @@ func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) mu Network: "tcp", }.Listen(ctx) if err != nil { - err := fmt.Errorf("Error finding port: %s", err) + err := fmt.Errorf("Error finding VNC port: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt @@ -49,9 +66,15 @@ func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) mu s.l.Listener.Close() // free port, but don't unlock lock file vncPort := s.l.Port + if config.VNCUsePassword { + vncPassword = VNCPassword() + } else { + vncPassword = "" + } + log.Printf("Found available VNC port: %d on IP: %s", vncPort, config.VNCBindAddress) state.Put("vnc_port", vncPort) - state.Put("vnc_ip", config.VNCBindAddress) + state.Put("vnc_password", vncPassword) return multistep.ActionContinue } diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go index 19b168aad..d5f3030ae 100644 --- a/builder/qemu/step_run.go +++ b/builder/qemu/step_run.go @@ -62,12 +62,10 @@ func (s *stepRun) Cleanup(state multistep.StateBag) { func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error) { config := state.Get("config").(*Config) isoPath := state.Get("iso_path").(string) - vncIP := state.Get("vnc_ip").(string) + vncIP := config.VNCBindAddress vncPort := state.Get("vnc_port").(int) ui := state.Get("ui").(packer.Ui) driver := state.Get("driver").(Driver) - - vnc := fmt.Sprintf("%s:%d", vncIP, vncPort-5900) vmName := config.VMName imgPath := filepath.Join(config.OutputDir, vmName) @@ -75,6 +73,13 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error var deviceArgs []string var driveArgs []string var sshHostPort int + var vnc string + + if !config.VNCUsePassword { + vnc = fmt.Sprintf("%s:%d", vncIP, vncPort-5900) + } else { + vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort-5900) + } defaultArgs["-name"] = vmName defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) @@ -141,17 +146,23 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error deviceArgs = append(deviceArgs, fmt.Sprintf("%s,netdev=user.0", config.NetDevice)) if config.Headless == true { - vncIpRaw, vncIpOk := state.GetOk("vnc_ip") vncPortRaw, vncPortOk := state.GetOk("vnc_port") + vncPass := state.Get("vnc_password") - if vncIpOk && vncPortOk { - vncIp := vncIpRaw.(string) + if vncPortOk && vncPass != nil && len(vncPass.(string)) > 0 { + vncPort := vncPortRaw.(int) + + ui.Message(fmt.Sprintf( + "The VM will be run headless, without a GUI. If you want to\n"+ + "view the screen of the VM, connect via VNC to vnc://%s:%d\n"+ + "with the password: %s", vncIP, vncPort, vncPass)) + } else if vncPortOk { vncPort := vncPortRaw.(int) ui.Message(fmt.Sprintf( "The VM will be run headless, without a GUI. If you want to\n"+ "view the screen of the VM, connect via VNC without a password to\n"+ - "vnc://%s:%d", vncIp, vncPort)) + "vnc://%s:%d", vncIP, vncPort)) } else { ui.Message("The VM will be run headless, without a GUI, as configured.\n" + "If the run isn't succeeding as you expect, please enable the GUI\n" + diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go index b524967cb..4bce10d0c 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -41,7 +41,8 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) httpPort := state.Get("http_port").(int) ui := state.Get("ui").(packer.Ui) vncPort := state.Get("vnc_port").(int) - vncIP := state.Get("vnc_ip").(string) + vncIP := config.VNCBindAddress + vncPassword := state.Get("vnc_password") if config.VNCConfig.DisableVNC { log.Println("Skipping boot command step...") @@ -76,7 +77,15 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) } defer nc.Close() - c, err := vnc.Client(nc, &vnc.ClientConfig{Exclusive: false}) + 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) diff --git a/website/source/docs/builders/qemu.html.md.erb b/website/source/docs/builders/qemu.html.md.erb index f9ab3de9a..4dc0b5f28 100644 --- a/website/source/docs/builders/qemu.html.md.erb +++ b/website/source/docs/builders/qemu.html.md.erb @@ -397,6 +397,10 @@ default port of `5985` or whatever value you have the service set to listen on. Packer uses a randomly chosen port in this range that appears available. By default this is `5900` to `6000`. The minimum and maximum ports are inclusive. +- `vnc_use_password` (bool) - Whether or not to set a password on the VNC server. + This option automatically sets `qmp_enable` to true. + Defaults to `false`. + ## Boot Command The `boot_command` configuration is very important: it specifies the keys to