Commit 3bc0c4aa authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

builder/qemu: simplify driver, make things more Go-like

parent d78787e1
...@@ -372,7 +372,19 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -372,7 +372,19 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepHTTPServer), new(stepHTTPServer),
new(stepForwardSSH), new(stepForwardSSH),
new(stepConfigureVNC), new(stepConfigureVNC),
new(stepRun), &stepRun{
BootDrive: "d",
Message: "Starting VM, booting from CD-ROM",
},
&stepBootWait{},
&stepTypeBootCommand{},
&stepWaitForShutdown{
Message: "Waiting for initial VM boot to shut down",
},
&stepRun{
BootDrive: "c",
Message: "Starting VM, booting from hard disk",
},
&common.StepConnectSSH{ &common.StepConnectSSH{
SSHAddress: sshAddress, SSHAddress: sshAddress,
SSHConfig: sshConfig, SSHConfig: sshConfig,
......
...@@ -3,7 +3,6 @@ package qemu ...@@ -3,7 +3,6 @@ package qemu
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"io" "io"
...@@ -11,7 +10,8 @@ import ( ...@@ -11,7 +10,8 @@ import (
"os/exec" "os/exec"
"regexp" "regexp"
"strings" "strings"
"time" "sync"
"syscall"
"unicode" "unicode"
) )
...@@ -25,21 +25,14 @@ type Driver interface { ...@@ -25,21 +25,14 @@ type Driver interface {
// qemuImgPath - string value for the qemu-img executable // qemuImgPath - string value for the qemu-img executable
Initialize(string, string) Initialize(string, string)
// Checks if the VM with the given name is running.
IsRunning(string) (bool, error)
// Stop stops a running machine, forcefully. // Stop stops a running machine, forcefully.
Stop(string) error Stop() error
// Qemu executes the given command via qemu-system-x86_64 // Qemu executes the given command via qemu-system-x86_64
Qemu(vmName string, qemuArgs ...string) error Qemu(qemuArgs ...string) error
// wait on shutdown of the VM with option to cancel // wait on shutdown of the VM with option to cancel
WaitForShutdown( WaitForShutdown(<-chan struct{}) bool
vmName string,
block bool,
state multistep.StateBag,
cancellCallback DriverCancelCallback) error
// Qemu executes the given command via qemu-img // Qemu executes the given command via qemu-img
QemuImg(...string) error QemuImg(...string) error
...@@ -53,156 +46,108 @@ type Driver interface { ...@@ -53,156 +46,108 @@ type Driver interface {
Version() (string, error) Version() (string, error)
} }
type driverState struct {
cmd *exec.Cmd
cancelChan chan struct{}
waitDone chan error
}
type QemuDriver struct { type QemuDriver struct {
qemuPath string qemuPath string
qemuImgPath string qemuImgPath string
state map[string]*driverState
}
func (d *QemuDriver) getDriverState(name string) *driverState { vmCmd *exec.Cmd
if _, ok := d.state[name]; !ok { vmEndCh <-chan int
d.state[name] = &driverState{} lock sync.Mutex
}
return d.state[name]
} }
func (d *QemuDriver) Initialize(qemuPath string, qemuImgPath string) { func (d *QemuDriver) Initialize(qemuPath string, qemuImgPath string) {
d.qemuPath = qemuPath d.qemuPath = qemuPath
d.qemuImgPath = qemuImgPath d.qemuImgPath = qemuImgPath
d.state = make(map[string]*driverState)
} }
func (d *QemuDriver) IsRunning(name string) (bool, error) { func (d *QemuDriver) Stop() error {
ds := d.getDriverState(name) d.lock.Lock()
return ds.cancelChan != nil, nil defer d.lock.Unlock()
}
func (d *QemuDriver) Stop(name string) error {
ds := d.getDriverState(name)
// signal to the command 'wait' to kill the process if d.vmCmd != nil {
if ds.cancelChan != nil { if err := d.vmCmd.Process.Kill(); err != nil {
close(ds.cancelChan) return err
ds.cancelChan = nil }
} }
return nil return nil
} }
func (d *QemuDriver) Qemu(vmName string, qemuArgs ...string) error { func (d *QemuDriver) Qemu(qemuArgs ...string) error {
d.lock.Lock()
defer d.lock.Unlock()
if d.vmCmd != nil {
panic("Existing VM state found")
}
stdout_r, stdout_w := io.Pipe() stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe() stderr_r, stderr_w := io.Pipe()
log.Printf("Executing %s: %#v", d.qemuPath, qemuArgs) log.Printf("Executing %s: %#v", d.qemuPath, qemuArgs)
ds := d.getDriverState(vmName) cmd := exec.Command(d.qemuPath, qemuArgs...)
ds.cmd = exec.Command(d.qemuPath, qemuArgs...) cmd.Stdout = stdout_w
ds.cmd.Stdout = stdout_w cmd.Stderr = stderr_w
ds.cmd.Stderr = stderr_w
err := cmd.Start()
if err != nil {
err = fmt.Errorf("Error starting VM: %s", err)
return err
}
go logReader("Qemu stdout", stdout_r) go logReader("Qemu stdout", stdout_r)
go logReader("Qemu stderr", stderr_r) go logReader("Qemu stderr", stderr_r)
err := ds.cmd.Start() log.Printf("Started Qemu. Pid: %d", cmd.Process.Pid)
if err != nil { // Wait for Qemu to complete in the background, and mark when its done
err = fmt.Errorf("Error starting VM: %s", err) endCh := make(chan int, 1)
} else { go func() {
log.Printf("---- Started Qemu ------- PID = %d", ds.cmd.Process.Pid) defer stderr_w.Close()
defer stdout_w.Close()
var exitCode int = 0
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
} else {
exitCode = 254
}
}
}
ds.cancelChan = make(chan struct{}) endCh <- exitCode
// make the channel to watch the process d.lock.Lock()
ds.waitDone = make(chan error) defer d.lock.Unlock()
d.vmCmd = nil
d.vmEndCh = nil
}()
// start the virtual machine in the background // Setup our state so we know we are running
go func() { d.vmCmd = cmd
defer stderr_w.Close() d.vmEndCh = endCh
defer stdout_w.Close()
ds.waitDone <- ds.cmd.Wait()
}()
}
return err return nil
} }
func (d *QemuDriver) WaitForShutdown(vmName string, func (d *QemuDriver) WaitForShutdown(cancelCh <-chan struct{}) bool {
block bool, d.lock.Lock()
state multistep.StateBag, endCh := d.vmEndCh
cancelCallback DriverCancelCallback) error { d.lock.Unlock()
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 if endCh == nil {
// and therefore the reason for failure is clear return true
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 select {
return err case <-endCh:
return true
case <-cancelCh:
return false
}
} }
func (d *QemuDriver) QemuImg(args ...string) error { func (d *QemuDriver) QemuImg(args ...string) error {
......
package qemu
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"time"
)
// stepBootWait waits the configured time period.
type stepBootWait struct{}
func (s *stepBootWait) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui)
if int64(config.bootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.bootWait))
time.Sleep(config.bootWait)
}
return multistep.ActionContinue
}
func (s *stepBootWait) Cleanup(state multistep.StateBag) {}
...@@ -6,49 +6,51 @@ import ( ...@@ -6,49 +6,51 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
) )
// stepRun runs the virtual machine
type stepRun struct { type stepRun struct {
vmName string BootDrive string
Message string
} }
func runBootCommand(state multistep.StateBag, func (s *stepRun) Run(state multistep.StateBag) multistep.StepAction {
actionChannel chan multistep.StepAction) { driver := state.Get("driver").(Driver)
config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
bootCmd := stepTypeBootCommand{}
if int64(config.bootWait) > 0 { ui.Say(s.Message)
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.bootWait))
time.Sleep(config.bootWait) command := getCommandArgs(s.BootDrive, state)
if err := driver.Qemu(command...); err != nil {
err := fmt.Errorf("Error launching VM: %s", err)
ui.Error(err.Error())
return multistep.ActionHalt
} }
actionChannel <- bootCmd.Run(state) return multistep.ActionContinue
} }
func cancelCallback(state multistep.StateBag) bool { func (s *stepRun) Cleanup(state multistep.StateBag) {
cancel := false driver := state.Get("driver").(Driver)
if _, ok := state.GetOk(multistep.StateCancelled); ok { ui := state.Get("ui").(packer.Ui)
cancel = true
if err := driver.Stop(); err != nil {
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
} }
return cancel
} }
func (s *stepRun) getCommandArgs( func getCommandArgs(bootDrive string, state multistep.StateBag) []string {
bootDrive string,
state multistep.StateBag) []string {
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(*config) 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) isoPath := state.Get("iso_path").(string)
vncPort := state.Get("vnc_port").(uint) vncPort := state.Get("vnc_port").(uint)
guiArgument := "sdl"
sshHostPort := state.Get("sshHostPort").(uint) sshHostPort := state.Get("sshHostPort").(uint)
ui := state.Get("ui").(packer.Ui)
guiArgument := "sdl"
vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900) vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900)
vmName := config.VMName
imgPath := filepath.Join(config.OutputDir,
fmt.Sprintf("%s.%s", vmName, strings.ToLower(config.Format)))
if config.Headless == true { if config.Headless == true {
ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" + ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" +
...@@ -112,74 +114,3 @@ func (s *stepRun) getCommandArgs( ...@@ -112,74 +114,3 @@ func (s *stepRun) getCommandArgs(
return outArgs 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))
}
}
}
...@@ -17,7 +17,6 @@ import ( ...@@ -17,7 +17,6 @@ import (
// config *config // config *config
// driver Driver // driver Driver
// ui packer.Ui // ui packer.Ui
// vmName string
// //
// Produces: // Produces:
// <nothing> // <nothing>
...@@ -28,7 +27,6 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -28,7 +27,6 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config) config := state.Get("config").(*config)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vmName := config.VMName
if config.ShutdownCommand != "" { if config.ShutdownCommand != "" {
ui.Say("Gracefully halting virtual machine...") ui.Say("Gracefully halting virtual machine...")
...@@ -41,28 +39,23 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -41,28 +39,23 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt return multistep.ActionHalt
} }
// Wait for the machine to actually shut down // Start the goroutine that will time out our graceful attempt
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout) cancelCh := make(chan struct{}, 1)
shutdownTimer := time.After(config.shutdownTimeout) go func() {
for { defer close(cancelCh)
running, _ := driver.IsRunning(vmName) <-time.After(config.shutdownTimeout)
if !running { }()
break
}
select { log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
case <-shutdownTimer: if ok := driver.WaitForShutdown(cancelCh); !ok {
err := errors.New("Timeout while waiting for machine to shut down.") err := errors.New("Timeout while waiting for machine to shut down.")
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
default:
time.Sleep(1 * time.Second)
}
} }
} else { } else {
ui.Say("Halting the virtual machine...") ui.Say("Halting the virtual machine...")
if err := driver.Stop(vmName); err != nil { if err := driver.Stop(); err != nil {
err := fmt.Errorf("Error stopping VM: %s", err) err := fmt.Errorf("Error stopping VM: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
......
package qemu
import (
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"time"
)
// stepWaitForShutdown waits for the shutdown of the currently running
// qemu VM.
type stepWaitForShutdown struct {
Message string
}
func (s *stepWaitForShutdown) Run(state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
stopCh := make(chan struct{})
defer close(stopCh)
cancelCh := make(chan struct{})
go func() {
for {
if _, ok := state.GetOk(multistep.StateCancelled); ok {
close(cancelCh)
return
}
select {
case <-stopCh:
return
case <-time.After(100 * time.Millisecond):
}
}
}()
ui.Say(s.Message)
driver.WaitForShutdown(cancelCh)
return multistep.ActionContinue
}
func (s *stepWaitForShutdown) Cleanup(state multistep.StateBag) {}
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