Skip to content

Latest commit

 

History

History
285 lines (244 loc) · 8.31 KB

README.md

File metadata and controls

285 lines (244 loc) · 8.31 KB

VmTest Build Status

VmTest is a Go language library that provides an easy way to setup a Virtual Machine for your integration tests written in Go.

Running Linux kernel in QEMU

In the following example vmtest sets up and lunches an instance of QEMU emulator with the kernel and initramfs specified.

package main

import (
	"fmt"
	"regexp"
	"testing"
	"time"

	"github.com/anatol/vmtest"
)

func TestBootCurrentLinuxKernelInQemu(t *testing.T) {
	// Configure QEMU emulator
	opts := vmtest.QemuOptions{
		OperatingSystem: vmtest.OS_LINUX,
		Kernel:          "/boot/vmlinuz-linux",
		InitRamFs:       "/boot/initramfs-linux.img",
		Params: []string{
			"-enable-kvm", "-cpu", "host",
			"-m", "8G",
		},
		Append: []string{
			"rd.luks.name=d4440324-32ed-44e6-a99f-5c18859b6bac=cryptroot",
			"root=/dev/mapper/cryptroot",
		},
		Disks: []vmtest.QemuDisk{
			{"testdata/luksv2.disk", "raw"},
		},
		Verbose: testing.Verbose(),
		Timeout: 20 * time.Second,
	}
	// Run QEMU instance
	qemu, err := vmtest.NewQemu(&opts)
	if err != nil {
		t.Fatal(err)
	}
	// Exit QEMU at the end of the test case
	defer qemu.Kill()

	// Wait until a specific string is found in the console output
	if err := qemu.ConsoleExpect("Run /init as init process"); err != nil {
		t.Fatal(err)
	}
	// Write some text to console
	if err := qemu.ConsoleWrite("12345"); err != nil {
		t.Fatal(err)
	}

	// Test the regexp matcher
	re, err := regexp.Compile("exit_code=(\\d+)")
	if err != nil {
		t.Fatal(err)
	}
	matches, err := qemu.ConsoleExpectRE(re)
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("Exit code is %v", matches[0])

	// Wait for some text again
	if err := qemu.ConsoleExpect("You are in emergency mode. After logging in, type \"journalctl -xb\" to view"); err != nil {
		t.Fatal(err)
	}
}

Then run the test using go test toolset:

$ go test
PASS
ok  	github.com/anatol/vmtest	2.495s

With the verbose mode specified one gets a lot of additional information from QEMU console:

$ go test -v
=== RUN   TestBootCurrentLinuxKernelInQemu
2020/04/04 11:15:25 QEMU command line: qemu-system-x86_64 -enable-kvm -cpu host -monitor unix:/tmp/vmtest012738181/monitor.socket -serial unix:/tmp/vmtest012738181/console.socket -no-reboot -m 8G -nographic -display none -device e1000,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5555-:22 -device virtio-scsi-pci,id=scsi -append 'console=ttyS0,115200 ignore_loglevel' -kernel /boot/vmlinuz-linux -initrd /boot/initramfs-linux.img
SeaBIOS (version ?-20191223_100556-anatol)
iPXE (http://ipxe.org) 00:03.0 CA00 PCI2.10 PnP PMM+BFF927B0+BFEF27B0 CA00
Press Ctrl-B to configure iPXE (PCI 00:03.0)...
Booting from ROM...
Probing EDD (edd=off to disable)... o[    0.000000] Linux version 5.5.13-arch2-1 (linux@archlinux) (gcc version 9.3.0 (Arch Linux 9.3.0-1)) #1 SMP PREEMPT Mon, 30 Mar 2020 20:42:41 +0000
[    0.000000] Command line: console=ttyS0,115200 ignore_loglevel
[    0.000000] KERNEL supported cpus:
[    0.000000]   Intel GenuineIntel
[    0.000000]   AMD AuthenticAMD
[    0.000000]   Hygon HygonGenuine
[    0.000000]   Centaur CentaurHauls
[    0.000000]   zhaoxin   Shanghai
[    0.000000] x86/fpu: Supporting XSAVE feature 0x001: 'x87 floating point registers'
[    0.000000] x86/fpu: Supporting XSAVE feature 0x002: 'SSE registers'
[    0.000000] x86/fpu: Supporting XSAVE feature 0x004: 'AVX registers'
[    0.000000] x86/fpu: Supporting XSAVE feature 0x008: 'MPX bounds registers'
....
[  OK  ] Stopped Create list of sta… nodes for the current kernel.
[    2.269008] audit: type=1131 audit(1586105934.576:7): pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=initrd-cleanup comm="systemd" exe="/init" hostname=? addr=? terminal=? res=success'
[    2.276930] audit: type=1131 audit(1586105934.589:8): pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=systemd-udevd comm="systemd" exe="/init" hostname=? addr=? terminal=? res=success'
[  OK  ] Finished Cleanup udevd DB.
[  OK  ] Reached target Switch Root.
[    2.294760] audit: type=1131 audit(1586105934.599:9): pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=systemd-tmpfiles-setup-dev comm="systemd" exe="/init" hostname=? addr=? terminal=? res=success'
         Starting Switch Root...
[FAILED] Failed to start Switch Root.
See 'systemctl status initrd-switch-root.service' for details.
[    2.314979] audit: type=1131 audit(1586105934.606:10): pid=1 uid=0 auid=4294967295 ses=4294967295 msg='unit=kmod-static-nodes comm="systemd" exe="/init" hostname=? addr=? terminal=? res=success'
You are in emergency mode. After logging in, type "journalctl -xb" to view
system logs, "systemctl reboot" to reboot, "systemctl default" or "exit"
to boot into default mode.
Press Enter for maintenance
--- PASS: TestBootCurrentLinuxKernelInQemu (2.92s)
PASS
ok  	github.com/anatol/vmtest	2.919s

Running ARM bare-metal application in QEMU

VmTest provides a way to test bare-metal application as well. In the following example we run ARM bare-metal app and verify that console contains expected output

package main

import (
	"testing"
	"time"

	"github.com/anatol/vmtest"
)

func TestRunArmInQemu(t *testing.T) {
	opts := vmtest.QemuOptions{
		Architecture: vmtest.QEMU_ARM,
		Params: []string{
			"-M", "versatilepb", "-m", "128M",
		},
		Kernel:  "testdata/hello-arm.bin",
		Verbose: testing.Verbose(),
		Timeout: 5 * time.Second,
	}
	qemu, err := vmtest.NewQemu(&opts)
	if err != nil {
		t.Fatal(err)
	}
	defer qemu.Kill()

	if err := qemu.ConsoleExpect("Hello from ARM emulator!"); err != nil {
		t.Fatal(err)
	}
}

Running tests as root inside a QEMU virtual machine

Some applications require testing with root privileges. VmTest library provides a way to do it by running a QEMU virtual machine, scp a test binary to it and then run the test there.

To achieve it please create a Linux kernel binary and rootfs image using following instructions.

Then create a test case that is going to run as a root inside QEMU vm:

package tests

import (
	"os/user"
	"testing"
)

func TestFoo(t *testing.T) {
	u, err := user.Current()
	if err != nil {
		t.Fatal(err)
	}

	if u.Username != "root" {
		t.SkipNow()
	}

	// add your tests here
}

Then add a test to setup a QEMU vm instance:

package tests

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"testing"
	"time"

	"github.com/anatol/vmtest"
	"github.com/tmc/scp"
	"golang.org/x/crypto/ssh"
)


func TestBootInQemu(t *testing.T) {
	// qemu_run_test.go is your test file from above. We have to compile it as a binary and copy to a QEMU vm
	cmd := exec.Command("go", "test", "-c", "qemu_run_test.go", "-o", "qemu_run_test")
	if testing.Verbose() {
		log.Print("compile in-qemu test binary")
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	}
	err := cmd.Run()
	if err != nil {
		t.Fatal(err)
	}
	defer os.Remove("qemu_run_test")

	// These integration tests use QEMU with a statically-compiled kernel (to avoid inintramfs) and specially
	// prepared rootfs. See TESTING.md.md file for instructions how to prepare
	opts := vmtest.QemuOptions{
		OperatingSystem: vmtest.OS_LINUX,
		Kernel:          "bzImage",
		Params:          []string{"-net", "user,hostfwd=tcp::10022-:22", "-net", "nic", "-enable-kvm", "-cpu", "host"},
		Disks:           []vmtest.QemuDisk{{"rootfs.qcow2", "qcow2"}},
		Append:          []string{"root=/dev/sda", "rw"},
		Verbose:         testing.Verbose(),
		Timeout:         50 * time.Second,
	}
	// Run QEMU instance
	qemu, err := vmtest.NewQemu(&opts)
	if err != nil {
		t.Fatal(err)
	}
	// Shutdown QEMU at the end of the test case
	defer qemu.Shutdown()

	config := &ssh.ClientConfig{
		User:            "root",
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	conn, err := ssh.Dial("tcp", "localhost:10022", config)
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()

	sess, err := conn.NewSession()
	if err != nil {
		t.Fatal(err)
	}
	defer sess.Close()

	scpSess, err := conn.NewSession()
	if err != nil {
		t.Fatal(err)
	}

	err = scp.CopyPath("qemu_run_test", "qemu_run_test", scpSess)
	if err != nil {
		t.Error(err)
	}

	testCmd := "./qemu_run_test"
	if testing.Verbose() {
		testCmd += " -test.v"
	}

	output, err := sess.CombinedOutput(testCmd)
	if testing.Verbose() {
		fmt.Print(string(output))
	}
	if err != nil {
		t.Fatal(err)
	}
}

License

See LICENSE.