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
new(stepHTTPServer),
new(stepForwardSSH),
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{
SSHAddress: sshAddress,
SSHConfig: sshConfig,
......
......@@ -3,7 +3,6 @@ package qemu
import (
"bufio"
"bytes"
"errors"
"fmt"
"github.com/mitchellh/multistep"
"io"
......@@ -11,7 +10,8 @@ import (
"os/exec"
"regexp"
"strings"
"time"
"sync"
"syscall"
"unicode"
)
......@@ -25,21 +25,14 @@ type Driver interface {
// 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
Stop() error
// 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
WaitForShutdown(
vmName string,
block bool,
state multistep.StateBag,
cancellCallback DriverCancelCallback) error
WaitForShutdown(<-chan struct{}) bool
// Qemu executes the given command via qemu-img
QemuImg(...string) error
......@@ -53,156 +46,108 @@ type Driver interface {
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]
vmCmd *exec.Cmd
vmEndCh <-chan int
lock sync.Mutex
}
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)
func (d *QemuDriver) Stop() error {
d.lock.Lock()
defer d.lock.Unlock()
// signal to the command 'wait' to kill the process
if ds.cancelChan != nil {
close(ds.cancelChan)
ds.cancelChan = nil
if d.vmCmd != nil {
if err := d.vmCmd.Process.Kill(); err != nil {
return err
}
}
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()
stderr_r, stderr_w := io.Pipe()
log.Printf("Executing %s: %#v", d.qemuPath, qemuArgs)
ds := d.getDriverState(vmName)
ds.cmd = exec.Command(d.qemuPath, qemuArgs...)
ds.cmd.Stdout = stdout_w
ds.cmd.Stderr = stderr_w
go logReader("Qemu stdout", stdout_r)
go logReader("Qemu stderr", stderr_r)
err := ds.cmd.Start()
cmd := exec.Command(d.qemuPath, qemuArgs...)
cmd.Stdout = stdout_w
cmd.Stderr = stderr_w
err := cmd.Start()
if err != nil {
err = fmt.Errorf("Error starting VM: %s", err)
} else {
log.Printf("---- Started Qemu ------- PID = %d", ds.cmd.Process.Pid)
return err
}
ds.cancelChan = make(chan struct{})
go logReader("Qemu stdout", stdout_r)
go logReader("Qemu stderr", stderr_r)
// make the channel to watch the process
ds.waitDone = make(chan error)
log.Printf("Started Qemu. Pid: %d", cmd.Process.Pid)
// start the virtual machine in the background
// Wait for Qemu to complete in the background, and mark when its done
endCh := make(chan int, 1)
go func() {
defer stderr_w.Close()
defer stdout_w.Close()
ds.waitDone <- ds.cmd.Wait()
}()
}
return err
}
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
}
}
}
func (d *QemuDriver) WaitForShutdown(vmName string,
block bool,
state multistep.StateBag,
cancelCallback DriverCancelCallback) error {
var err error
endCh <- exitCode
ds := d.getDriverState(vmName)
d.lock.Lock()
defer d.lock.Unlock()
d.vmCmd = nil
d.vmEndCh = nil
}()
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)
}
// Setup our state so we know we are running
d.vmCmd = cmd
d.vmEndCh = endCh
// 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)
func (d *QemuDriver) WaitForShutdown(cancelCh <-chan struct{}) bool {
d.lock.Lock()
endCh := d.vmEndCh
d.lock.Unlock()
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)
if endCh == nil {
return true
}
// 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
}
}()
select {
case <-endCh:
return true
case <-cancelCh:
return false
}
ds.cancelChan = nil
return err
}
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 (
"github.com/mitchellh/packer/packer"
"path/filepath"
"strings"
"time"
)
// stepRun runs the virtual machine
type stepRun struct {
vmName string
BootDrive string
Message string
}
func runBootCommand(state multistep.StateBag,
actionChannel chan multistep.StepAction) {
config := state.Get("config").(*config)
func (s *stepRun) Run(state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
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)
ui.Say(s.Message)
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 {
cancel := false
if _, ok := state.GetOk(multistep.StateCancelled); ok {
cancel = true
func (s *stepRun) Cleanup(state multistep.StateBag) {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
if err := driver.Stop(); err != nil {
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
}
return cancel
}
func (s *stepRun) getCommandArgs(
bootDrive string,
state multistep.StateBag) []string {
ui := state.Get("ui").(packer.Ui)
func getCommandArgs(bootDrive string, state multistep.StateBag) []string {
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)
ui := state.Get("ui").(packer.Ui)
guiArgument := "sdl"
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 {
ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" +
......@@ -112,74 +114,3 @@ func (s *stepRun) getCommandArgs(
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 (
// config *config
// driver Driver
// ui packer.Ui
// vmName string
//
// Produces:
// <nothing>
......@@ -28,7 +27,6 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
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...")
......@@ -41,28 +39,23 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
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
}
// Start the goroutine that will time out our graceful attempt
cancelCh := make(chan struct{}, 1)
go func() {
defer close(cancelCh)
<-time.After(config.shutdownTimeout)
}()
select {
case <-shutdownTimer:
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
if ok := driver.WaitForShutdown(cancelCh); !ok {
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 {
if err := driver.Stop(); err != nil {
err := fmt.Errorf("Error stopping VM: %s", err)
state.Put("error", err)
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