Skip to content

Commit

Permalink
Merge pull request #152 from glabelderp/feature/qemuTeststep
Browse files Browse the repository at this point in the history
plugins/teststeps/qemu.go: Add qemu teststep.
  • Loading branch information
mimir-d committed May 6, 2023
2 parents 24bc5aa + 26b0a06 commit c919523
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 0 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/gin-gonic/gin v1.8.1
github.com/go-sql-driver/mysql v1.6.0
github.com/google/go-safeweb v0.0.0-20211026121254-697f59a9d57f
github.com/google/goexpect v0.0.0-20200703111054-623d5ca06f56
github.com/google/uuid v1.3.0
github.com/insomniacslk/termhook v0.0.0-20210329134026-a267c978e590
github.com/insomniacslk/xjson v0.0.0-20210106140854-1589ccfd1a1a
Expand Down Expand Up @@ -49,6 +50,7 @@ require (
github.com/goccy/go-json v0.9.8 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
github.com/hugelgupf/p9 v0.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down Expand Up @@ -86,5 +88,6 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/grpc v1.31.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -648,6 +649,7 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
Expand Down
56 changes: 56 additions & 0 deletions plugins/teststeps/qemu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Qemu Teststep


## Parameters

### Required Parameters
* **executable:** Name of the qemu executable. It can be an absolute path or the name of a executable in $PATH.

* **firmware:** The firmware image you want to test.


### Optional Paramters
* **logfile:** The output of the running image is copied here. If left empty the output will be discarded by setting the logfile to /dev/null.

* **mem:** The amount of RAM dedicated to the qemu instance in MB.

* **nproc:** The amount of threads available to qemu.

* **image:** A Disk Image, which can be booted by the firmware.

* **timeout:** The time intervall until the qemu instance is forcibly shut down. Example: '4m'

* **steps:** This is a list of steps, which can consist of expect or send steps. An expect steps expects a certain output from the virtual machine. A send step will send a string to qemu. Expect steps can have an additional timeout field, which is a string, like '2m'. If left empty the **timeout** Parameter is used as timeout instead. Make sure the timout you set for an expect step is shorter than the overall timeout. A step can have both an expect as well as a send statement; this is interpreted as an expect step, which is followed by a send step if it is successful. The steps are executed in order beginning from the first entry. Each step is blocking, meaning the next step will be executed only if the previous step was successful.
Example:
steps:
- expect: 'Welcome .*please login:'
timeout: 3s
send: username
- expect: Password
- send: secretPassword
- expect: Login successful
Notice that regular expressions have to be surrounded by single quotes.

- name: qemu
label: awesome test
parameters:
executable: ['qemu-system-aarch64]
firmware: ['/my/awesome/firmware']
image: ['/home/user1/images/Linux.qcow2']
nproc: [8]
mem: [8000]
logfile: [/tmp/Logfile]
timeout: [4m]
steps:
- expect: Booting into OS
timeout: 4s
- expect: '\nKernel'
- expect: login
- send: user
- expect: Password
- timeout: 5s
- send: 12345password
- expect: user@
- timeout: 10s
- send: poweroff
- expect: Power down
277 changes: 277 additions & 0 deletions plugins/teststeps/qemu/qemu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
package qemu

import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"time"

expect "github.com/google/goexpect"
"github.com/linuxboot/contest/pkg/event"
"github.com/linuxboot/contest/pkg/event/testevent"
"github.com/linuxboot/contest/pkg/target"
"github.com/linuxboot/contest/pkg/test"
"github.com/linuxboot/contest/pkg/xcontext"
"github.com/linuxboot/contest/plugins/teststeps"
)

const (
defaultTimeout = "10m"
defaultNproc = "3"
defaultMemory = "5000"
)

// Name of the plugin
var Name = "Qemu"

var Events = []event.Name{}

type Qemu struct {
executable *test.Param
firmware *test.Param
nproc *test.Param
mem *test.Param
image *test.Param
logfile *test.Param
timeout *test.Param
steps []test.Param
}

// Needed for the Teststep interface. Returns a Teststep instance.
func New() test.TestStep {
return &Qemu{}
}

// Needed for the Teststep interface. Returns the Name, the New() Function and
// the events the teststep can emit (which are no events).
func Load() (string, test.TestStepFactory, []event.Name) {
return Name, New, Events
}

// Name returns the name of the Step
func (q Qemu) Name() string {
return Name
}

// ValidateParameters validates the parameters that will be passed to the Run
// and Resume methods of the test step.
func (q *Qemu) ValidateParameters(_ xcontext.Context, params test.TestStepParameters) error {
if q.executable = params.GetOne("executable"); q.executable.IsEmpty() {
return fmt.Errorf("No Qemu executable given")
}

if q.firmware = params.GetOne("firmware"); q.firmware.IsEmpty() {
return errors.New("Missing 'firmware' field in qemu parameters")
}

if q.nproc = params.GetOne("nproc"); q.nproc.IsEmpty() {
q.nproc = test.NewParam(defaultNproc)
}
if q.mem = params.GetOne("mem"); q.mem.IsEmpty() {
q.mem = test.NewParam(defaultMemory)
}
if q.timeout = params.GetOne("timeout"); q.timeout.IsEmpty() {
q.timeout = test.NewParam(defaultTimeout)
}

q.image = params.GetOne("image")

q.steps = params.Get("steps")

q.logfile = params.GetOne("logfile")

return nil
}

func (q *Qemu) validateAndPopulate(ctx xcontext.Context, params test.TestStepParameters) error {
return q.ValidateParameters(ctx, params)
}

// Run starts the Qemu instance for each target and interacts with the qemu instance
// through the expect and send steps.
func (q *Qemu) Run(ctx xcontext.Context,
ch test.TestStepChannels,
ev testevent.Emitter,
stepsVars test.StepsVariables,
params test.TestStepParameters,
resumeState json.RawMessage,
) (json.RawMessage, error) {
log := ctx.Logger()

if err := q.validateAndPopulate(ctx, params); err != nil {
return nil, err
}
f := func(ctx xcontext.Context, target *target.Target) error {
targetTimeout, err := q.timeout.Expand(target, stepsVars)
if err != nil {
return err
}

targetLogfile, err := q.logfile.Expand(target, stepsVars)
if err != nil {
return err
}

targetImage, err := q.image.Expand(target, stepsVars)
if err != nil {
return err
}

targetQemu, err := q.executable.Expand(target, stepsVars)
if err != nil {
return err
}

// basic checks whether the executable is usable
if abs := filepath.IsAbs(targetQemu); !abs {
_, err := exec.LookPath(targetQemu)
if err != nil {
return fmt.Errorf("unable to find qemu executable in PATH: %w", err)
}
}

targetFirmware, err := q.firmware.Expand(target, stepsVars)
if err != nil {
return err
}

targetMem, err := q.mem.Expand(target, stepsVars)
if err != nil {
return err
}

targetNproc, err := q.nproc.Expand(target, stepsVars)
if err != nil {
return err
}

globalTimeout, err := time.ParseDuration(targetTimeout)
if err != nil {
return fmt.Errorf("Could not Parse %v as Timeout: %w", targetTimeout, err)
}

// no graphical output and no network access
command := []string{targetQemu, "-nographic", "-nic", "none", "-bios", targetFirmware}
qemuOpts := []string{"-m", targetMem, "-smp", targetNproc}

command = append(command, qemuOpts...)
if targetImage != "" {
command = append(command, targetImage)
}

var logfile *os.File
if targetLogfile != "" {

logfile, err = os.Create(targetLogfile)
if err != nil {
return fmt.Errorf("Could not create Logfile: %w", err)
}
defer logfile.Close()
} else {
logfile, err = os.OpenFile("/dev/null", os.O_WRONLY, fs.ModeDevice)
if err != nil {
return fmt.Errorf("Could not redirect output to '/dev/null': %w", err)
}
defer logfile.Close()
}

gExpect, errchan, err := expect.SpawnWithArgs(
command,
globalTimeout,
expect.Tee(logfile),
expect.CheckDuration(time.Minute),
expect.PartialMatch(false),
expect.SendTimeout(globalTimeout),
)
if err != nil {
return fmt.Errorf("Could not start qemu: %w", err)
}

log.Infof("Started Qemu with command: %v", command)

defer gExpect.Close()

defer func() {
select {
case err = <-errchan:
log.Errorf("Error from Qemu: %v", err)
default:

}
}()

// struct to capture expect and send strings from json.
type expector struct {
Expect string
Send string
Timeout string
}

// loop over all steps and expect/ send the given strings
for _, interaction := range q.steps {

dst := new(expector)

jsString, err := interaction.Expand(target, stepsVars)
if err != nil {
return err
}

interactionParam := test.NewParam(jsString)
js := interactionParam.JSON()

if err := json.Unmarshal(js, dst); err != nil {
return fmt.Errorf("Could not Unmarshal steps: %w", err)
}

// Expect and Send fields must not both be empty
if dst.Expect+dst.Send == "" {
return fmt.Errorf("%s is not a valid step statement", interaction.String())
}

// process expect step
if dst.Expect != "" {

var timeout time.Duration
if dst.Timeout == "" {
timeout = globalTimeout
} else {
if timeout, err = time.ParseDuration(dst.Timeout); err != nil {
return fmt.Errorf("Could not parse timeout '%s' for step: '%s'. %w", dst.Timeout, dst.Expect, err)
}
}

if _, _, err := gExpect.Expect(regexp.MustCompile(dst.Expect), timeout); err != nil {
return fmt.Errorf("Error while expecting '%s': %w", dst.Expect, err)
}

log.Debugf("Completed expect step: '%v' with timeout: %v \n", dst.Expect, timeout.String())

}

// process send step
if dst.Send != "" {

if err := gExpect.Send(dst.Send + "\n"); err != nil {
return fmt.Errorf("Unable to send '%s': %w", dst.Send, err)
}

// notify the user if the timeout field is used incorrectly
if dst.Expect == "" && dst.Timeout != "" {
log.Warnf("The Timeout %v for send step: %v will be ignored.", dst.Timeout, dst.Send)
}

log.Debugf("Completed Send Step: '%v'", dst.Send)
}
}

log.Infof("Matching steps successful")
return nil
}
return teststeps.ForEachTarget(Name, ctx, ch, f)
}

0 comments on commit c919523

Please sign in to comment.