Add VNC Password support to QEMU builder

This commit is contained in:
Jayson Cofell 2019-07-03 18:38:56 -06:00
parent afe9ba2869
commit 5c5943b8ba
7 changed files with 132 additions and 25 deletions

View File

@ -126,6 +126,7 @@ type Config struct {
VNCBindAddress string `mapstructure:"vnc_bind_address"` VNCBindAddress string `mapstructure:"vnc_bind_address"`
VNCPortMin int `mapstructure:"vnc_port_min"` VNCPortMin int `mapstructure:"vnc_port_min"`
VNCPortMax int `mapstructure:"vnc_port_max"` VNCPortMax int `mapstructure:"vnc_port_max"`
VNCUsePassword bool `mapstructure:"vnc_use_password"`
VMName string `mapstructure:"vm_name"` VMName string `mapstructure:"vm_name"`
// These are deprecated, but we keep them around for BC // 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")) 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 == "" { if b.config.QMPEnable && b.config.QMPSocketPath == "" {
socketName := fmt.Sprintf("%s.monitor", b.config.VMName) socketName := fmt.Sprintf("%s.monitor", b.config.VMName)
b.config.QMPSocketPath = filepath.Join(b.config.OutputDir, socketName) b.config.QMPSocketPath = filepath.Join(b.config.OutputDir, socketName)

View File

@ -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) { func TestBuilderPrepare_DiskCompaction(t *testing.T) {
var b Builder var b Builder
config := testConfig() config := testConfig()

View File

@ -30,27 +30,63 @@ func (s *stepConfigureQMP) Run(ctx context.Context, state multistep.StateBag) mu
return multistep.ActionContinue 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) ui.Say(msg)
log.Print(msg) log.Print(msg)
// Open QMP socket // Only initialize and open QMP when we have a use for it.
var err error // Handles cases where user may want the socket, but we don't
s.monitor, err = qmp.NewSocketMonitor("unix", config.QMPSocketPath, 2*time.Second) if config.VNCUsePassword {
if err != nil { // Open QMP socket
err := fmt.Errorf("Error opening QMP socket: %s", err) var err error
state.Put("error", err) var cmd []byte
ui.Error(err.Error()) var result []byte
return multistep.ActionHalt 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 return multistep.ActionContinue
} }
func (s *stepConfigureQMP) Cleanup(multistep.StateBag) { 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)
}
}
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"math/rand"
"github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/common/net"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
@ -22,6 +23,21 @@ type stepConfigureVNC struct {
l *net.Listener 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 { func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
@ -33,6 +49,7 @@ func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) mu
ui.Say(msg) ui.Say(msg)
log.Print(msg) log.Print(msg)
var vncPassword string
var err error var err error
s.l, err = net.ListenRangeConfig{ s.l, err = net.ListenRangeConfig{
Addr: config.VNCBindAddress, Addr: config.VNCBindAddress,
@ -41,7 +58,7 @@ func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) mu
Network: "tcp", Network: "tcp",
}.Listen(ctx) }.Listen(ctx)
if err != nil { if err != nil {
err := fmt.Errorf("Error finding port: %s", err) err := fmt.Errorf("Error finding VNC port: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt 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 s.l.Listener.Close() // free port, but don't unlock lock file
vncPort := s.l.Port vncPort := s.l.Port
if config.VNCUsePassword {
vncPassword = VNCPassword()
} else {
vncPassword = ""
}
log.Printf("Found available VNC port: %d on IP: %s", vncPort, config.VNCBindAddress) log.Printf("Found available VNC port: %d on IP: %s", vncPort, config.VNCBindAddress)
state.Put("vnc_port", vncPort) state.Put("vnc_port", vncPort)
state.Put("vnc_ip", config.VNCBindAddress) state.Put("vnc_password", vncPassword)
return multistep.ActionContinue return multistep.ActionContinue
} }

View File

@ -62,12 +62,10 @@ func (s *stepRun) Cleanup(state multistep.StateBag) {
func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error) { func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error) {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
isoPath := state.Get("iso_path").(string) isoPath := state.Get("iso_path").(string)
vncIP := state.Get("vnc_ip").(string) vncIP := config.VNCBindAddress
vncPort := state.Get("vnc_port").(int) vncPort := state.Get("vnc_port").(int)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
vnc := fmt.Sprintf("%s:%d", vncIP, vncPort-5900)
vmName := config.VMName vmName := config.VMName
imgPath := filepath.Join(config.OutputDir, vmName) imgPath := filepath.Join(config.OutputDir, vmName)
@ -75,6 +73,13 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
var deviceArgs []string var deviceArgs []string
var driveArgs []string var driveArgs []string
var sshHostPort int 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["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) 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)) deviceArgs = append(deviceArgs, fmt.Sprintf("%s,netdev=user.0", config.NetDevice))
if config.Headless == true { if config.Headless == true {
vncIpRaw, vncIpOk := state.GetOk("vnc_ip")
vncPortRaw, vncPortOk := state.GetOk("vnc_port") vncPortRaw, vncPortOk := state.GetOk("vnc_port")
vncPass := state.Get("vnc_password")
if vncIpOk && vncPortOk { if vncPortOk && vncPass != nil && len(vncPass.(string)) > 0 {
vncIp := vncIpRaw.(string) 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) vncPort := vncPortRaw.(int)
ui.Message(fmt.Sprintf( ui.Message(fmt.Sprintf(
"The VM will be run headless, without a GUI. If you want to\n"+ "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"+ "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 { } else {
ui.Message("The VM will be run headless, without a GUI, as configured.\n" + 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" + "If the run isn't succeeding as you expect, please enable the GUI\n" +

View File

@ -41,7 +41,8 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
httpPort := state.Get("http_port").(int) httpPort := state.Get("http_port").(int)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vncPort := state.Get("vnc_port").(int) vncPort := state.Get("vnc_port").(int)
vncIP := state.Get("vnc_ip").(string) vncIP := config.VNCBindAddress
vncPassword := state.Get("vnc_password")
if config.VNCConfig.DisableVNC { if config.VNCConfig.DisableVNC {
log.Println("Skipping boot command step...") log.Println("Skipping boot command step...")
@ -76,7 +77,15 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
} }
defer nc.Close() 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 { if err != nil {
err := fmt.Errorf("Error handshaking with VNC: %s", err) err := fmt.Errorf("Error handshaking with VNC: %s", err)
state.Put("error", err) state.Put("error", err)

View File

@ -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 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. 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 ## Boot Command
The `boot_command` configuration is very important: it specifies the keys to The `boot_command` configuration is very important: it specifies the keys to