Commit ac1012c1 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge pull request #385 from TranscendComputing/master

builder/qemu: Qemu builder
parents 3cd00ad5 9ebd44a9
package qemu
import (
"fmt"
"os"
)
// Artifact is the result of running the Qemu builder, namely a set
// of files associated with the resulting machine.
type Artifact struct {
dir string
f []string
}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (a *Artifact) Files() []string {
return a.f
}
func (*Artifact) Id() string {
return "VM"
}
func (a *Artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *Artifact) Destroy() error {
return os.RemoveAll(a.dir)
}
This diff is collapsed.
This diff is collapsed.
package qemu
import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"log"
"os/exec"
"regexp"
"strings"
"time"
)
type DriverCancelCallback func(state multistep.StateBag) bool
// A driver is able to talk to qemu-system-x86_64 and perform certain
// operations with it.
type Driver interface {
// Initializes the driver with the given values:
// Arguments: qemuPath - string value for the qemu-system-x86_64 executable
// qemuImgPath - string value for the qemu-img executable
Initialize(string, string)
// Checks if the VM with the given name is running.
IsRunning(string) (bool, error)
// Stop stops a running machine, forcefully.
Stop(string) error
// SuppressMessages should do what needs to be done in order to
// suppress any annoying popups, if any.
SuppressMessages() error
// Qemu executes the given command via qemu-system-x86_64
Qemu(vmName string, qemuArgs ...string) error
// wait on shutdown of the VM with option to cancel
WaitForShutdown(
vmName string,
block bool,
state multistep.StateBag,
cancellCallback DriverCancelCallback) error
// Qemu executes the given command via qemu-img
QemuImg(...string) error
// Verify checks to make sure that this driver should function
// properly. If there is any indication the driver can't function,
// this will return an error.
Verify() error
// Version reads the version of Qemu that is installed.
Version() (string, error)
}
type driverState struct {
cmd *exec.Cmd
cancelChan chan struct{}
waitDone chan error
}
type QemuDriver struct {
qemuPath string
qemuImgPath string
state map[string]*driverState
}
func (d *QemuDriver) getDriverState(name string) *driverState {
if _, ok := d.state[name]; !ok {
d.state[name] = &driverState{}
}
return d.state[name]
}
func (d *QemuDriver) Initialize(qemuPath string, qemuImgPath string) {
d.qemuPath = qemuPath
d.qemuImgPath = qemuImgPath
d.state = make(map[string]*driverState)
}
func (d *QemuDriver) IsRunning(name string) (bool, error) {
ds := d.getDriverState(name)
return ds.cancelChan != nil, nil
}
func (d *QemuDriver) Stop(name string) error {
ds := d.getDriverState(name)
// signal to the command 'wait' to kill the process
if ds.cancelChan != nil {
close(ds.cancelChan)
ds.cancelChan = nil
}
return nil
}
func (d *QemuDriver) SuppressMessages() error {
return nil
}
func (d *QemuDriver) Qemu(vmName string, qemuArgs ...string) error {
var stdout, stderr bytes.Buffer
log.Printf("Executing %s: %#v", d.qemuPath, qemuArgs)
ds := d.getDriverState(vmName)
ds.cmd = exec.Command(d.qemuPath, qemuArgs...)
ds.cmd.Stdout = &stdout
ds.cmd.Stderr = &stderr
err := ds.cmd.Start()
if err != nil {
err = fmt.Errorf("Error starting VM: %s", err)
} else {
log.Printf("---- Started Qemu ------- PID = ", ds.cmd.Process.Pid)
ds.cancelChan = make(chan struct{})
// make the channel to watch the process
ds.waitDone = make(chan error)
// start the virtual machine in the background
go func() {
ds.waitDone <- ds.cmd.Wait()
}()
}
return err
}
func (d *QemuDriver) WaitForShutdown(vmName string,
block bool,
state multistep.StateBag,
cancelCallback DriverCancelCallback) error {
var err error
ds := d.getDriverState(vmName)
if block {
// wait in the background for completion or caller cancel
for {
select {
case <-ds.cancelChan:
log.Println("Qemu process request to cancel -- killing Qemu process.")
if err = ds.cmd.Process.Kill(); err != nil {
log.Printf("Failed to kill qemu: %v", err)
}
// clear out the error channel since it's just a cancel
// and therefore the reason for failure is clear
log.Println("Empytying waitDone channel.")
<-ds.waitDone
// this gig is over -- assure calls to IsRunning see the nil
log.Println("'Nil'ing out cancelChan.")
ds.cancelChan = nil
return errors.New("WaitForShutdown cancelled")
case err = <-ds.waitDone:
log.Printf("Qemu Process done with output = %v", err)
// assure calls to IsRunning see the nil
log.Println("'Nil'ing out cancelChan.")
ds.cancelChan = nil
return nil
case <-time.After(1 * time.Second):
cancel := cancelCallback(state)
if cancel {
log.Println("Qemu process request to cancel -- killing Qemu process.")
// The step sequence was cancelled, so cancel waiting for SSH
// and just start the halting process.
close(ds.cancelChan)
log.Println("Cancel request made, quitting waiting for Qemu.")
return errors.New("WaitForShutdown cancelled by interrupt.")
}
}
}
} else {
go func() {
select {
case <-ds.cancelChan:
log.Println("Qemu process request to cancel -- killing Qemu process.")
if err = ds.cmd.Process.Kill(); err != nil {
log.Printf("Failed to kill qemu: %v", err)
}
// clear out the error channel since it's just a cancel
// and therefore the reason for failure is clear
log.Println("Empytying waitDone channel.")
<-ds.waitDone
log.Println("'Nil'ing out cancelChan.")
ds.cancelChan = nil
case err = <-ds.waitDone:
log.Printf("Qemu Process done with output = %v", err)
log.Println("'Nil'ing out cancelChan.")
ds.cancelChan = nil
}
}()
}
ds.cancelChan = nil
return err
}
func (d *QemuDriver) QemuImg(args ...string) error {
var stdout, stderr bytes.Buffer
log.Printf("Executing qemu-img: %#v", args)
cmd := exec.Command(d.qemuImgPath, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("QemuImg error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return err
}
func (d *QemuDriver) Verify() error {
return nil
}
func (d *QemuDriver) Version() (string, error) {
var stdout bytes.Buffer
cmd := exec.Command(d.qemuPath, "-version")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
versionOutput := strings.TrimSpace(stdout.String())
log.Printf("Qemu --version output: %s", versionOutput)
versionRe := regexp.MustCompile("qemu-kvm-[0-9]\\.[0-9]")
matches := versionRe.Split(versionOutput, 2)
if len(matches) == 0 {
return "", fmt.Errorf("No version found: %s", versionOutput)
}
log.Printf("Qemu version: %s", matches[0])
return matches[0], nil
}
package qemu
import (
gossh "code.google.com/p/go.crypto/ssh"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/communicator/ssh"
"io/ioutil"
"os"
)
func sshAddress(state multistep.StateBag) (string, error) {
sshHostPort := state.Get("sshHostPort").(uint)
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
}
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
config := state.Get("config").(*config)
auth := []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
}
if config.SSHKeyPath != "" {
keyring, err := sshKeyToKeyring(config.SSHKeyPath)
if err != nil {
return nil, err
}
auth = append(auth, gossh.ClientAuthKeyring(keyring))
}
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: auth,
}, nil
}
func sshKeyToKeyring(path string) (gossh.ClientKeyring, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
keyring := new(ssh.SimpleKeychain)
if err := keyring.AddPEMKey(string(keyBytes)); err != nil {
return nil, err
}
return keyring, nil
}
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
"net"
)
// This step configures the VM to enable the VNC server.
//
// Uses:
// config *config
// ui packer.Ui
//
// Produces:
// vnc_port uint - The port that VNC is configured to listen on.
type stepConfigureVNC struct{}
func (stepConfigureVNC) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui)
// Find an open VNC port. Note that this can still fail later on
// because we have to release the port at some point. But this does its
// best.
msg := fmt.Sprintf("Looking for available port between %d and %d", config.VNCPortMin, config.VNCPortMax)
ui.Say(msg)
log.Printf(msg)
var vncPort uint
portRange := int(config.VNCPortMax - config.VNCPortMin)
for {
vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin
log.Printf("Trying port: %d", vncPort)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", vncPort))
if err == nil {
defer l.Close()
break
}
}
msg = fmt.Sprintf("Found available VNC port: %d", vncPort)
ui.Say(msg)
log.Printf(msg)
state.Put("vnc_port", vncPort)
return multistep.ActionContinue
}
func (stepConfigureVNC) Cleanup(multistep.StateBag) {}
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
)
// This step attaches the ISO to the virtual machine.
//
// Uses:
//
// Produces:
type stepCopyFloppy struct {
floppyPath string
}
func (s *stepCopyFloppy) Run(state multistep.StateBag) multistep.StepAction {
// Determine if we even have a floppy disk to attach
var floppyPath string
if floppyPathRaw, ok := state.GetOk("floppy_path"); ok {
floppyPath = floppyPathRaw.(string)
} else {
log.Println("No floppy disk, not attaching.")
return multistep.ActionContinue
}
// copy the floppy for exclusive use during the vm creation
ui := state.Get("ui").(packer.Ui)
ui.Say("Copying floppy disk for exclusive use...")
floppyPath, err := s.copyFloppy(floppyPath)
if err != nil {
state.Put("error", fmt.Errorf("Error preparing floppy: %s", err))
return multistep.ActionHalt
}
// Track the path so that we can remove it later
s.floppyPath = floppyPath
return multistep.ActionContinue
}
func (s *stepCopyFloppy) Cleanup(state multistep.StateBag) {
if s.floppyPath == "" {
return
}
// Delete the floppy disk
ui := state.Get("ui").(packer.Ui)
ui.Say("Removing floppy disk previously copied...")
defer os.Remove(s.floppyPath)
}
func (s *stepCopyFloppy) copyFloppy(path string) (string, error) {
tempdir, err := ioutil.TempDir("", "packer")
if err != nil {
return "", err
}
floppyPath := filepath.Join(tempdir, "floppy.img")
f, err := os.Create(floppyPath)
if err != nil {
return "", err
}
defer f.Close()
sourceF, err := os.Open(path)
if err != nil {
return "", err
}
defer sourceF.Close()
log.Printf("Copying floppy to temp location: %s", floppyPath)
if _, err := io.Copy(f, sourceF); err != nil {
return "", err
}
return floppyPath, nil
}
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"path/filepath"
"strings"
)
// This step creates the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepCreateDisk struct{}
func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName,
strings.ToLower(config.Format)))
command := []string{
"create",
"-f", config.Format,
path,
fmt.Sprintf("%vM", config.DiskSize),
}
ui.Say("Creating hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {}
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
"net"
)
// This step adds a NAT port forwarding definition so that SSH is available
// on the guest machine.
//
// Uses:
//
// Produces:
type stepForwardSSH struct{}
func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui)
log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
var sshHostPort uint
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
for {
sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin
log.Printf("Trying port: %d", sshHostPort)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
if err == nil {
defer l.Close()
break
}
}
ui.Say(fmt.Sprintf("Found port for SSH: %d.", sshHostPort))
// Save the port we're using so that future steps can use it
state.Put("sshHostPort", sshHostPort)
return multistep.ActionContinue
}
func (s *stepForwardSSH) Cleanup(state multistep.StateBag) {}
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
"net"
"net/http"
)
// This step creates and runs the HTTP server that is serving the files
// specified by the 'http_files` configuration parameter in the template.
//
// Uses:
// config *config
// ui packer.Ui
//
// Produces:
// http_port int - The port the HTTP server started on.
type stepHTTPServer struct {
l net.Listener
}
func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui)
var httpPort uint = 0
if config.HTTPDir == "" {
state.Put("http_port", httpPort)
return multistep.ActionContinue
}
// Find an available TCP port for our HTTP server
var httpAddr string
portRange := int(config.HTTPPortMax - config.HTTPPortMin)
for {
var err error
var offset uint = 0
if portRange > 0 {
// Intn will panic if portRange == 0, so we do a check.
offset = uint(rand.Intn(portRange))
}
httpPort = offset + config.HTTPPortMin
httpAddr = fmt.Sprintf(":%d", httpPort)
log.Printf("Trying port: %d", httpPort)
s.l, err = net.Listen("tcp", httpAddr)
if err == nil {
break
}
}
ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort))
// Start the HTTP server and run it in the background
fileServer := http.FileServer(http.Dir(config.HTTPDir))
server := &http.Server{Addr: httpAddr, Handler: fileServer}
go server.Serve(s.l)
// Save the address into the state so it can be accessed in the future
state.Put("http_port", httpPort)
return multistep.ActionContinue
}
func (s *stepHTTPServer) Cleanup(multistep.StateBag) {
if s.l != nil {
// Close the listener so that the HTTP server stops
s.l.Close()
}
}
package qemu
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
"time"
)
type stepPrepareOutputDir struct{}
func (stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui)
if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce {
ui.Say("Deleting previous output directory...")
os.RemoveAll(config.OutputDir)
}
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) {
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting output directory...")
for i := 0; i < 5; i++ {
err := os.RemoveAll(config.OutputDir)
if err == nil {
break
}
log.Printf("Error removing output dir: %s", err)
time.Sleep(2 * time.Second)
}
}
}
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"path/filepath"
"strings"
"time"
)
type stepRun struct {
vmName string
}
func runBootCommand(state multistep.StateBag,
actionChannel chan multistep.StepAction) {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui)
bootCmd := stepTypeBootCommand{}
if int64(config.bootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.bootWait))
time.Sleep(config.bootWait)
}
actionChannel <- bootCmd.Run(state)
}
func cancelCallback(state multistep.StateBag) bool {
cancel := false
if _, ok := state.GetOk(multistep.StateCancelled); ok {
cancel = true
}
return cancel
}
func (s *stepRun) getCommandArgs(
bootDrive string,
state multistep.StateBag) []string {
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(*config)
vmName := config.VMName
imgPath := filepath.Join(config.OutputDir,
fmt.Sprintf("%s.%s", vmName, strings.ToLower(config.Format)))
isoPath := state.Get("iso_path").(string)
vncPort := state.Get("vnc_port").(uint)
guiArgument := "sdl"
sshHostPort := state.Get("sshHostPort").(uint)
vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900)
if config.Headless == true {
ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" +
"In headless mode, errors during the boot sequence or OS setup\n" +
"won't be easily visible. Use at your own discretion.")
guiArgument = "none"
}
defaultArgs := make(map[string]string)
defaultArgs["-name"] = vmName
defaultArgs["-machine"] = fmt.Sprintf("type=pc-1.0,accel=%s", config.Accelerator)
defaultArgs["-display"] = guiArgument
defaultArgs["-netdev"] = "user,id=user.0"
defaultArgs["-device"] = fmt.Sprintf("%s,netdev=user.0", config.NetDevice)
defaultArgs["-drive"] = fmt.Sprintf("file=%s,if=%s", imgPath, config.DiskInterface)
defaultArgs["-cdrom"] = isoPath
defaultArgs["-boot"] = bootDrive
defaultArgs["-m"] = "512m"
defaultArgs["-redir"] = fmt.Sprintf("tcp:%v::22", sshHostPort)
defaultArgs["-vnc"] = vnc
inArgs := make(map[string][]string)
if len(config.QemuArgs) > 0 {
ui.Say("Overriding defaults Qemu arguments with QemuArgs...")
// becuase qemu supports multiple appearances of the same
// switch, just different values, each key in the args hash
// will have an array of string values
for _, qemuArgs := range config.QemuArgs {
key := qemuArgs[0]
val := strings.Join(qemuArgs[1:], "")
if _, ok := inArgs[key]; !ok {
inArgs[key] = make([]string, 0)
}
if len(val) > 0 {
inArgs[key] = append(inArgs[key], val)
}
}
}
// get any remaining missing default args from the default settings
for key := range defaultArgs {
if _, ok := inArgs[key]; !ok {
arg := make([]string, 1)
arg[0] = defaultArgs[key]
inArgs[key] = arg
}
}
// Flatten to array of strings
outArgs := make([]string, 0)
for key, values := range inArgs {
if len(values) > 0 {
for idx := range values {
outArgs = append(outArgs, key, values[idx])
}
} else {
outArgs = append(outArgs, key)
}
}
return outArgs
}
func (s *stepRun) runVM(
sendBootCommands bool,
bootDrive string,
state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
vmName := config.VMName
ui.Say("Starting the virtual machine for OS Install...")
command := s.getCommandArgs(bootDrive, state)
if err := driver.Qemu(vmName, command...); err != nil {
err := fmt.Errorf("Error launching VM: %s", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.vmName = vmName
// run the boot command after its own timeout
if sendBootCommands {
waitDone := make(chan multistep.StepAction, 1)
go runBootCommand(state, waitDone)
select {
case action := <-waitDone:
if action != multistep.ActionContinue {
// stop the VM in its tracks
driver.Stop(vmName)
return multistep.ActionHalt
}
}
}
ui.Say("Waiting for VM to shutdown...")
if err := driver.WaitForShutdown(vmName, sendBootCommands, state, cancelCallback); err != nil {
err := fmt.Errorf("Error waiting for initial VM install to shutdown: %s", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepRun) Run(state multistep.StateBag) multistep.StepAction {
// First, the OS install boot
action := s.runVM(true, "d", state)
if action == multistep.ActionContinue {
// Then the provisioning install
action = s.runVM(false, "c", state)
}
return action
}
func (s *stepRun) Cleanup(state multistep.StateBag) {
if s.vmName == "" {
return
}
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
if running, _ := driver.IsRunning(s.vmName); running {
if err := driver.Stop(s.vmName); err != nil {
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
}
}
}
package qemu
import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"time"
)
// This step shuts down the machine. It first attempts to do so gracefully,
// but ultimately forcefully shuts it down if that fails.
//
// Uses:
// communicator packer.Communicator
// config *config
// driver Driver
// ui packer.Ui
// vmName string
//
// Produces:
// <nothing>
type stepShutdown struct{}
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
vmName := config.VMName
if config.ShutdownCommand != "" {
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
if err := cmd.StartWithUi(comm, ui); err != nil {
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the machine to actually shut down
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
shutdownTimer := time.After(config.shutdownTimeout)
for {
running, _ := driver.IsRunning(vmName)
if !running {
break
}
select {
case <-shutdownTimer:
err := errors.New("Timeout while waiting for machine to shut down.")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
default:
time.Sleep(1 * time.Second)
}
}
} else {
ui.Say("Halting the virtual machine...")
if err := driver.Stop(vmName); err != nil {
err := fmt.Errorf("Error stopping VM: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
log.Println("VM shut down.")
return multistep.ActionContinue
}
func (s *stepShutdown) Cleanup(state multistep.StateBag) {}
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
)
// This step sets some variables in Qemu so that annoying
// pop-up messages don't exist.
type stepSuppressMessages struct{}
func (stepSuppressMessages) Run(state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
log.Println("Suppressing messages in Qemu")
if err := driver.SuppressMessages(); err != nil {
err := fmt.Errorf("Error configuring Qemu to suppress messages: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (stepSuppressMessages) Cleanup(state multistep.StateBag) {}
package qemu
import (
"fmt"
"github.com/mitchellh/go-vnc"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"net"
"strings"
"time"
"unicode"
"unicode/utf8"
)
const KeyLeftShift uint32 = 0xFFE1
type bootCommandTemplateData struct {
HTTPIP string
HTTPPort uint
Name string
}
// This step "types" the boot command into the VM over VNC.
//
// Uses:
// config *config
// http_port int
// ui packer.Ui
// vnc_port uint
//
// Produces:
// <nothing>
type stepTypeBootCommand struct{}
func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
httpPort := state.Get("http_port").(uint)
ui := state.Get("ui").(packer.Ui)
vncPort := state.Get("vnc_port").(uint)
// Connect to VNC
ui.Say("Connecting to VM via VNC")
nc, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", 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()
c, err := vnc.Client(nc, &vnc.ClientConfig{Exclusive: true})
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)
tplData := &bootCommandTemplateData{
"127.0.0.1",
httpPort,
config.VMName,
}
ui.Say("Typing the boot command over VNC...")
for _, command := range config.BootCommand {
command, err := config.tpl.Process(command, tplData)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Check for interrupts between typing things so we can cancel
// since this isn't the fastest thing.
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return multistep.ActionHalt
}
vncSendString(c, command)
}
return multistep.ActionContinue
}
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
func vncSendString(c *vnc.ClientConn, original string) {
special := make(map[string]uint32)
special["<bs>"] = 0xFF08
special["<del>"] = 0xFFFF
special["<enter>"] = 0xFF0D
special["<esc>"] = 0xFF1B
special["<f1>"] = 0xFFBE
special["<f2>"] = 0xFFBF
special["<f3>"] = 0xFFC0
special["<f4>"] = 0xFFC1
special["<f5>"] = 0xFFC2
special["<f6>"] = 0xFFC3
special["<f7>"] = 0xFFC4
special["<f8>"] = 0xFFC5
special["<f9>"] = 0xFFC6
special["<f10>"] = 0xFFC7
special["<f11>"] = 0xFFC8
special["<f12>"] = 0xFFC9
special["<return>"] = 0xFF0D
special["<tab>"] = 0xFF09
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
// TODO(mitchellh): Ripe for optimizations of some point, perhaps.
for len(original) > 0 {
var keyCode uint32
keyShift := false
if strings.HasPrefix(original, "<wait>") {
log.Printf("Special code '<wait>' found, sleeping one second")
time.Sleep(1 * time.Second)
original = original[len("<wait>"):]
continue
}
if strings.HasPrefix(original, "<wait5>") {
log.Printf("Special code '<wait5>' found, sleeping 5 seconds")
time.Sleep(5 * time.Second)
original = original[len("<wait5>"):]
continue
}
if strings.HasPrefix(original, "<wait10>") {
log.Printf("Special code '<wait10>' found, sleeping 10 seconds")
time.Sleep(10 * time.Second)
original = original[len("<wait10>"):]
continue
}
for specialCode, specialValue := range special {
if strings.HasPrefix(original, specialCode) {
log.Printf("Special code '%s' found, replacing with: %d", specialCode, specialValue)
keyCode = specialValue
original = original[len(specialCode):]
break
}
}
if keyCode == 0 {
r, size := utf8.DecodeRuneInString(original)
original = original[size:]
keyCode = uint32(r)
keyShift = unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r)
log.Printf("Sending char '%c', code %d, shift %v", r, keyCode, keyShift)
}
if keyShift {
c.KeyEvent(KeyLeftShift, true)
}
c.KeyEvent(keyCode, true)
c.KeyEvent(keyCode, false)
if keyShift {
c.KeyEvent(KeyLeftShift, false)
}
// qemu is picky, so no matter what, wait a small period
time.Sleep(100 * time.Millisecond)
}
}
......@@ -24,6 +24,7 @@ const defaultConfig = `
"amazon-instance": "packer-builder-amazon-instance",
"digitalocean": "packer-builder-digitalocean",
"openstack": "packer-builder-openstack",
"qemu": "packer-builder-qemu",
"virtualbox": "packer-builder-virtualbox",
"vmware": "packer-builder-vmware"
},
......
package main
import (
"github.com/mitchellh/packer/builder/qemu"
"github.com/mitchellh/packer/packer/plugin"
)
func main() {
plugin.ServeBuilder(new(qemu.Builder))
}
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment