269 lines
6.6 KiB
Go
269 lines
6.6 KiB
Go
package qemu
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/mitchellh/multistep"
|
|
"io"
|
|
"log"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
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
|
|
|
|
// 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) Qemu(vmName string, qemuArgs ...string) error {
|
|
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()
|
|
|
|
if err != nil {
|
|
err = fmt.Errorf("Error starting VM: %s", err)
|
|
} else {
|
|
log.Printf("---- Started Qemu ------- PID = %d", 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() {
|
|
defer stderr_w.Close()
|
|
defer stdout_w.Close()
|
|
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
|
|
}
|
|
|
|
func logReader(name string, r io.Reader) {
|
|
bufR := bufio.NewReader(r)
|
|
for {
|
|
line, err := bufR.ReadString('\n')
|
|
if line != "" {
|
|
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
|
log.Printf("%s: %s", name, line)
|
|
}
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
}
|