package qemu import ( "bufio" "bytes" "fmt" "io" "log" "os" "os/exec" "regexp" "strings" "sync" "syscall" "time" "unicode" "github.com/hashicorp/packer-plugin-sdk/multistep" ) 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 { // Copy bypasses qemu-img convert and directly copies an image // that doesn't need converting. Copy(string, string) error // Stop stops a running machine, forcefully. Stop() error // Qemu executes the given command via qemu-system-x86_64 Qemu(qemuArgs ...string) error // wait on shutdown of the VM with option to cancel WaitForShutdown(<-chan struct{}) bool // 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 QemuDriver struct { QemuPath string QemuImgPath string vmCmd *exec.Cmd vmEndCh <-chan int lock sync.Mutex } func (d *QemuDriver) Stop() error { d.lock.Lock() defer d.lock.Unlock() if d.vmCmd != nil { if err := d.vmCmd.Process.Kill(); err != nil { return err } } return nil } func (d *QemuDriver) Copy(sourceName, targetName string) error { source, err := os.Open(sourceName) if err != nil { err = fmt.Errorf("Error opening iso for copy: %s", err) return err } defer source.Close() // Create will truncate an existing file target, err := os.Create(targetName) if err != nil { err = fmt.Errorf("Error creating hard drive in output dir: %s", err) return err } defer target.Close() log.Printf("Copying %s to %s", source.Name(), target.Name()) bytes, err := io.Copy(target, source) if err != nil { err = fmt.Errorf("Error copying iso to output dir: %s", err) return err } log.Printf(fmt.Sprintf("Copied %d bytes", bytes)) return nil } 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) 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) return err } go logReader("Qemu stdout", stdout_r) go logReader("Qemu stderr", stderr_r) log.Printf("Started Qemu. Pid: %d", cmd.Process.Pid) // 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() 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 } } } endCh <- exitCode d.lock.Lock() defer d.lock.Unlock() d.vmCmd = nil d.vmEndCh = nil }() // Wait at least a couple seconds for an early fail from Qemu so // we can report that. select { case exit := <-endCh: if exit != 0 { return fmt.Errorf("Qemu failed to start. Please run with PACKER_LOG=1 to get more info.") } case <-time.After(2 * time.Second): } // Setup our state so we know we are running d.vmCmd = cmd d.vmEndCh = endCh return nil } func (d *QemuDriver) WaitForShutdown(cancelCh <-chan struct{}) bool { d.lock.Lock() endCh := d.vmEndCh d.lock.Unlock() if endCh == nil { return true } select { case <-endCh: return true case <-cancelCh: return false } } 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(`[\.[0-9]+]*`) matches := versionRe.FindStringSubmatch(versionOutput) 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 } } }