1
0
out-of-tree/qemu/qemu-kernel.go

341 lines
7.0 KiB
Go
Raw Normal View History

2018-09-18 21:45:21 +00:00
// Copyright 2018 Mikhail Klementev. All rights reserved.
2018-10-08 20:51:32 +00:00
// Use of this source code is governed by a AGPLv3 license
2018-09-18 21:45:21 +00:00
// (or later) that can be found in the LICENSE file.
package qemukernel
import (
2018-10-06 14:00:31 +00:00
"bytes"
2018-09-18 21:45:21 +00:00
"errors"
"fmt"
"io"
2018-09-19 06:13:28 +00:00
"math/rand"
"net"
2018-09-18 21:45:21 +00:00
"os"
"os/exec"
2018-09-20 23:47:36 +00:00
"runtime"
"strings"
2018-09-18 21:45:21 +00:00
"syscall"
"time"
"golang.org/x/crypto/ssh"
2018-09-18 21:45:21 +00:00
)
func readUntilEOF(pipe io.ReadCloser, buf *[]byte) (err error) {
2018-09-18 21:45:21 +00:00
bufSize := 1024
for err != io.EOF {
stdout := make([]byte, bufSize)
var n int
n, err = pipe.Read(stdout)
if err != nil && err != io.EOF {
return
}
*buf = append(*buf, stdout[:n]...)
2018-09-18 21:45:21 +00:00
}
if err == io.EOF {
err = nil
}
return
}
type arch string
const (
X86_64 arch = "x86_64"
I386 = "i386"
// TODO add other
unsupported = "unsupported" // for test purposes
)
// Kernel describe kernel parameters for qemu
type Kernel struct {
2018-09-22 10:34:43 +00:00
Name string
KernelPath string
InitrdPath string
2018-09-18 21:45:21 +00:00
}
// QemuSystem describe qemu parameters and runned process
type QemuSystem struct {
2018-09-19 06:13:28 +00:00
arch arch
kernel Kernel
drivePath string
Cpus int
Memory int
2018-09-18 21:45:21 +00:00
2018-09-22 07:28:08 +00:00
// Timeout works after Start invocation
Timeout time.Duration
KilledByTimeout bool
2018-10-06 14:00:48 +00:00
KernelPanic bool
2018-09-22 07:28:08 +00:00
Died bool
sshAddrPort string
2018-09-18 21:45:21 +00:00
// accessible while qemu is runned
cmd *exec.Cmd
pipe struct {
stdin io.WriteCloser
stderr io.ReadCloser
stdout io.ReadCloser
}
Stdout, Stderr []byte
2018-09-18 21:45:21 +00:00
// accessible after qemu is closed
exitErr error
2018-09-18 21:45:21 +00:00
}
// NewQemuSystem constructor
func NewQemuSystem(arch arch, kernel Kernel, drivePath string) (q *QemuSystem, err error) {
2018-09-18 21:45:21 +00:00
if _, err = exec.LookPath("qemu-system-" + string(arch)); err != nil {
return
}
q = &QemuSystem{}
2018-09-18 21:45:21 +00:00
q.arch = arch
2018-09-22 10:34:43 +00:00
if _, err = os.Stat(kernel.KernelPath); err != nil {
2018-09-18 21:45:21 +00:00
return
}
q.kernel = kernel
2018-09-19 06:13:28 +00:00
if _, err = os.Stat(drivePath); err != nil {
return
}
q.drivePath = drivePath
// Default values
2018-09-22 10:13:58 +00:00
q.Cpus = 1
2018-09-19 06:13:28 +00:00
q.Memory = 512 // megabytes
2018-09-18 21:45:21 +00:00
return
}
2018-09-19 06:13:28 +00:00
func getRandomAddrPort() (addr string) {
// 127.1-255.0-255.0-255:10000-50000
ip := fmt.Sprintf("127.%d.%d.%d",
rand.Int()%254+1, rand.Int()%255, rand.Int()%254)
port := rand.Int()%40000 + 10000
return fmt.Sprintf("%s:%d", ip, port)
}
2018-09-20 23:47:36 +00:00
func getRandomPort(ip string) (addr string) {
// ip:1024-65535
2018-10-08 18:59:05 +00:00
port := rand.Int()%(65536-1024) + 1024
2018-09-20 23:47:36 +00:00
return fmt.Sprintf("%s:%d", ip, port)
}
2018-09-19 06:13:28 +00:00
func getFreeAddrPort() (addrPort string) {
2018-09-20 23:47:36 +00:00
timeout := time.Now().Add(time.Second)
2018-09-19 06:13:28 +00:00
for {
2018-09-20 23:47:36 +00:00
if runtime.GOOS == "linux" {
addrPort = getRandomAddrPort()
} else {
addrPort = getRandomPort("127.0.0.1")
}
2018-09-19 06:13:28 +00:00
ln, err := net.Listen("tcp", addrPort)
if err == nil {
ln.Close()
return
}
2018-09-20 23:47:36 +00:00
if time.Now().After(timeout) {
panic("Can't found free address:port on localhost")
}
2018-09-19 06:13:28 +00:00
}
}
func kvmExists() bool {
if _, err := os.Stat("/dev/kvm"); err != nil {
return false
}
return true
}
2018-10-06 14:00:48 +00:00
func (q *QemuSystem) panicWatcher() {
for {
time.Sleep(time.Second)
if bytes.Contains(q.Stdout, []byte("Kernel panic")) {
time.Sleep(time.Second)
// There is no reason to stay alive after kernel panic
q.Stop()
q.KernelPanic = true
return
}
}
}
2018-09-18 21:45:21 +00:00
// Start qemu process
func (q *QemuSystem) Start() (err error) {
2018-09-19 06:13:28 +00:00
rand.Seed(time.Now().UnixNano()) // Are you sure?
q.sshAddrPort = getFreeAddrPort()
hostfwd := fmt.Sprintf("hostfwd=tcp:%s-:22", q.sshAddrPort)
2018-09-19 06:13:28 +00:00
qemuArgs := []string{"-snapshot", "-nographic",
"-hda", q.drivePath,
2018-09-22 10:34:43 +00:00
"-kernel", q.kernel.KernelPath,
"-append", "root=/dev/sda ignore_loglevel console=ttyS0 rw",
2018-09-19 06:13:28 +00:00
"-smp", fmt.Sprintf("%d", q.Cpus),
"-m", fmt.Sprintf("%d", q.Memory),
"-device", "e1000,netdev=n1",
"-netdev", "user,id=n1," + hostfwd,
2018-09-19 06:13:28 +00:00
}
2018-09-22 10:34:43 +00:00
if q.kernel.InitrdPath != "" {
qemuArgs = append(qemuArgs, "-initrd", q.kernel.InitrdPath)
}
2018-09-19 06:13:28 +00:00
if (q.arch == X86_64 || q.arch == I386) && kvmExists() {
qemuArgs = append(qemuArgs, "-enable-kvm")
}
2018-09-21 00:13:29 +00:00
if q.arch == X86_64 && runtime.GOOS == "darwin" {
qemuArgs = append(qemuArgs, "-accel", "hvf", "-cpu", "host")
}
2018-09-19 06:13:28 +00:00
q.cmd = exec.Command("qemu-system-"+string(q.arch), qemuArgs...)
2018-09-18 21:45:21 +00:00
if q.pipe.stdin, err = q.cmd.StdinPipe(); err != nil {
return
}
if q.pipe.stdout, err = q.cmd.StdoutPipe(); err != nil {
return
}
if q.pipe.stderr, err = q.cmd.StderrPipe(); err != nil {
return
}
err = q.cmd.Start()
if err != nil {
return
}
go readUntilEOF(q.pipe.stdout, &q.Stdout)
go readUntilEOF(q.pipe.stderr, &q.Stderr)
2018-09-18 21:45:21 +00:00
go func() {
q.exitErr = q.cmd.Wait()
2018-09-22 07:28:08 +00:00
q.Died = true
2018-09-18 21:45:21 +00:00
}()
time.Sleep(time.Second / 10) // wait for immediately die
2018-09-22 07:28:08 +00:00
if q.Died {
err = errors.New("qemu died immediately: " + string(q.Stderr))
2018-09-18 21:45:21 +00:00
}
2018-10-06 14:00:48 +00:00
go q.panicWatcher()
2018-09-22 07:28:08 +00:00
if q.Timeout != 0 {
go func() {
time.Sleep(q.Timeout)
q.KilledByTimeout = true
q.Stop()
}()
}
2018-09-18 21:45:21 +00:00
return
}
// Stop qemu process
func (q *QemuSystem) Stop() {
// 1 00/01 01 01 SOH (Ctrl-A) START OF HEADING
fmt.Fprintf(q.pipe.stdin, "%cx", 1)
// wait for die
time.Sleep(time.Second / 10)
2018-09-22 07:28:08 +00:00
if !q.Died {
2018-09-18 21:45:21 +00:00
q.cmd.Process.Signal(syscall.SIGTERM)
time.Sleep(time.Second / 10)
q.cmd.Process.Signal(syscall.SIGKILL)
}
}
2018-10-06 14:00:31 +00:00
func (q QemuSystem) ssh(user string) (client *ssh.Client, err error) {
cfg := &ssh.ClientConfig{
2018-10-06 14:00:31 +00:00
User: user,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
2018-10-06 14:00:31 +00:00
client, err = ssh.Dial("tcp", q.sshAddrPort, cfg)
return
}
// Command executes shell commands on qemu system
func (q QemuSystem) Command(user, cmd string) (output string, err error) {
client, err := q.ssh(user)
if err != nil {
return
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return
}
bytesOutput, err := session.CombinedOutput(cmd)
output = string(bytesOutput)
return
}
2018-10-06 14:00:31 +00:00
// AsyncCommand executes command on qemu system but does not wait for exit
func (q QemuSystem) AsyncCommand(user, cmd string) (err error) {
client, err := q.ssh(user)
if err != nil {
return
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return
}
return session.Run(fmt.Sprintf(
"nohup sh -c '%s' > /dev/null 2> /dev/null < /dev/null &", cmd))
}
// CopyFile is copy file from local machine to remote through ssh/scp
func (q *QemuSystem) CopyFile(user, localPath, remotePath string) (err error) {
addrPort := strings.Split(q.sshAddrPort, ":")
addr := addrPort[0]
port := addrPort[1]
2018-10-06 12:46:11 +00:00
cmd := exec.Command("scp", "-P", port,
"-o", "StrictHostKeyChecking=no",
"-o", "LogLevel=error",
localPath, user+"@"+addr+":"+remotePath)
output, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(output))
}
return
}
func (q *QemuSystem) CopyAndInsmod(localKoPath string) (output string, err error) {
remoteKoPath := fmt.Sprintf("/tmp/module_%d.ko", rand.Int())
err = q.CopyFile("root", localKoPath, remoteKoPath)
if err != nil {
return
}
return q.Command("root", "insmod "+remoteKoPath)
}
// CopyAndRun is copy local file to qemu vm then run it
func (q *QemuSystem) CopyAndRun(user, path string) (output string, err error) {
remotePath := fmt.Sprintf("/tmp/executable_%d", rand.Int())
err = q.CopyFile(user, path, remotePath)
if err != nil {
return
}
return q.Command(user, "chmod +x "+remotePath+" && "+remotePath)
}