-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #152 from glabelderp/feature/qemuTeststep
plugins/teststeps/qemu.go: Add qemu teststep.
- Loading branch information
Showing
4 changed files
with
338 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |