Skip to content

Commit c919523

Browse files
authored
Merge pull request #152 from glabelderp/feature/qemuTeststep
plugins/teststeps/qemu.go: Add qemu teststep.
2 parents 24bc5aa + 26b0a06 commit c919523

4 files changed

Lines changed: 338 additions & 0 deletions

File tree

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/gin-gonic/gin v1.8.1
1010
github.com/go-sql-driver/mysql v1.6.0
1111
github.com/google/go-safeweb v0.0.0-20211026121254-697f59a9d57f
12+
github.com/google/goexpect v0.0.0-20200703111054-623d5ca06f56
1213
github.com/google/uuid v1.3.0
1314
github.com/insomniacslk/termhook v0.0.0-20210329134026-a267c978e590
1415
github.com/insomniacslk/xjson v0.0.0-20210106140854-1589ccfd1a1a
@@ -49,6 +50,7 @@ require (
4950
github.com/goccy/go-json v0.9.8 // indirect
5051
github.com/golang/protobuf v1.5.2 // indirect
5152
github.com/golang/snappy v0.0.1 // indirect
53+
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
5254
github.com/hugelgupf/p9 v0.1.0 // indirect
5355
github.com/jmespath/go-jmespath v0.4.0 // indirect
5456
github.com/json-iterator/go v1.1.12 // indirect
@@ -86,5 +88,6 @@ require (
8688
golang.org/x/text v0.3.7 // indirect
8789
golang.org/x/tools v0.1.12 // indirect
8890
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
91+
google.golang.org/grpc v1.31.0 // indirect
8992
google.golang.org/protobuf v1.28.0 // indirect
9093
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
111111
github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
112112
github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
113113
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
114+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
114115
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
115116
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
116117
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -648,6 +649,7 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY
648649
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
649650
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
650651
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
652+
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
651653
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
652654
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
653655
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

plugins/teststeps/qemu/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Qemu Teststep
2+
3+
4+
## Parameters
5+
6+
### Required Parameters
7+
* **executable:** Name of the qemu executable. It can be an absolute path or the name of a executable in $PATH.
8+
9+
* **firmware:** The firmware image you want to test.
10+
11+
12+
### Optional Paramters
13+
* **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.
14+
15+
* **mem:** The amount of RAM dedicated to the qemu instance in MB.
16+
17+
* **nproc:** The amount of threads available to qemu.
18+
19+
* **image:** A Disk Image, which can be booted by the firmware.
20+
21+
* **timeout:** The time intervall until the qemu instance is forcibly shut down. Example: '4m'
22+
23+
* **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.
24+
Example:
25+
steps:
26+
- expect: 'Welcome .*please login:'
27+
timeout: 3s
28+
send: username
29+
- expect: Password
30+
- send: secretPassword
31+
- expect: Login successful
32+
Notice that regular expressions have to be surrounded by single quotes.
33+
34+
- name: qemu
35+
label: awesome test
36+
parameters:
37+
executable: ['qemu-system-aarch64]
38+
firmware: ['/my/awesome/firmware']
39+
image: ['/home/user1/images/Linux.qcow2']
40+
nproc: [8]
41+
mem: [8000]
42+
logfile: [/tmp/Logfile]
43+
timeout: [4m]
44+
steps:
45+
- expect: Booting into OS
46+
timeout: 4s
47+
- expect: '\nKernel'
48+
- expect: login
49+
- send: user
50+
- expect: Password
51+
- timeout: 5s
52+
- send: 12345password
53+
- expect: user@
54+
- timeout: 10s
55+
- send: poweroff
56+
- expect: Power down

plugins/teststeps/qemu/qemu.go

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
package qemu
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"regexp"
12+
"time"
13+
14+
expect "github.com/google/goexpect"
15+
"github.com/linuxboot/contest/pkg/event"
16+
"github.com/linuxboot/contest/pkg/event/testevent"
17+
"github.com/linuxboot/contest/pkg/target"
18+
"github.com/linuxboot/contest/pkg/test"
19+
"github.com/linuxboot/contest/pkg/xcontext"
20+
"github.com/linuxboot/contest/plugins/teststeps"
21+
)
22+
23+
const (
24+
defaultTimeout = "10m"
25+
defaultNproc = "3"
26+
defaultMemory = "5000"
27+
)
28+
29+
// Name of the plugin
30+
var Name = "Qemu"
31+
32+
var Events = []event.Name{}
33+
34+
type Qemu struct {
35+
executable *test.Param
36+
firmware *test.Param
37+
nproc *test.Param
38+
mem *test.Param
39+
image *test.Param
40+
logfile *test.Param
41+
timeout *test.Param
42+
steps []test.Param
43+
}
44+
45+
// Needed for the Teststep interface. Returns a Teststep instance.
46+
func New() test.TestStep {
47+
return &Qemu{}
48+
}
49+
50+
// Needed for the Teststep interface. Returns the Name, the New() Function and
51+
// the events the teststep can emit (which are no events).
52+
func Load() (string, test.TestStepFactory, []event.Name) {
53+
return Name, New, Events
54+
}
55+
56+
// Name returns the name of the Step
57+
func (q Qemu) Name() string {
58+
return Name
59+
}
60+
61+
// ValidateParameters validates the parameters that will be passed to the Run
62+
// and Resume methods of the test step.
63+
func (q *Qemu) ValidateParameters(_ xcontext.Context, params test.TestStepParameters) error {
64+
if q.executable = params.GetOne("executable"); q.executable.IsEmpty() {
65+
return fmt.Errorf("No Qemu executable given")
66+
}
67+
68+
if q.firmware = params.GetOne("firmware"); q.firmware.IsEmpty() {
69+
return errors.New("Missing 'firmware' field in qemu parameters")
70+
}
71+
72+
if q.nproc = params.GetOne("nproc"); q.nproc.IsEmpty() {
73+
q.nproc = test.NewParam(defaultNproc)
74+
}
75+
if q.mem = params.GetOne("mem"); q.mem.IsEmpty() {
76+
q.mem = test.NewParam(defaultMemory)
77+
}
78+
if q.timeout = params.GetOne("timeout"); q.timeout.IsEmpty() {
79+
q.timeout = test.NewParam(defaultTimeout)
80+
}
81+
82+
q.image = params.GetOne("image")
83+
84+
q.steps = params.Get("steps")
85+
86+
q.logfile = params.GetOne("logfile")
87+
88+
return nil
89+
}
90+
91+
func (q *Qemu) validateAndPopulate(ctx xcontext.Context, params test.TestStepParameters) error {
92+
return q.ValidateParameters(ctx, params)
93+
}
94+
95+
// Run starts the Qemu instance for each target and interacts with the qemu instance
96+
// through the expect and send steps.
97+
func (q *Qemu) Run(ctx xcontext.Context,
98+
ch test.TestStepChannels,
99+
ev testevent.Emitter,
100+
stepsVars test.StepsVariables,
101+
params test.TestStepParameters,
102+
resumeState json.RawMessage,
103+
) (json.RawMessage, error) {
104+
log := ctx.Logger()
105+
106+
if err := q.validateAndPopulate(ctx, params); err != nil {
107+
return nil, err
108+
}
109+
f := func(ctx xcontext.Context, target *target.Target) error {
110+
targetTimeout, err := q.timeout.Expand(target, stepsVars)
111+
if err != nil {
112+
return err
113+
}
114+
115+
targetLogfile, err := q.logfile.Expand(target, stepsVars)
116+
if err != nil {
117+
return err
118+
}
119+
120+
targetImage, err := q.image.Expand(target, stepsVars)
121+
if err != nil {
122+
return err
123+
}
124+
125+
targetQemu, err := q.executable.Expand(target, stepsVars)
126+
if err != nil {
127+
return err
128+
}
129+
130+
// basic checks whether the executable is usable
131+
if abs := filepath.IsAbs(targetQemu); !abs {
132+
_, err := exec.LookPath(targetQemu)
133+
if err != nil {
134+
return fmt.Errorf("unable to find qemu executable in PATH: %w", err)
135+
}
136+
}
137+
138+
targetFirmware, err := q.firmware.Expand(target, stepsVars)
139+
if err != nil {
140+
return err
141+
}
142+
143+
targetMem, err := q.mem.Expand(target, stepsVars)
144+
if err != nil {
145+
return err
146+
}
147+
148+
targetNproc, err := q.nproc.Expand(target, stepsVars)
149+
if err != nil {
150+
return err
151+
}
152+
153+
globalTimeout, err := time.ParseDuration(targetTimeout)
154+
if err != nil {
155+
return fmt.Errorf("Could not Parse %v as Timeout: %w", targetTimeout, err)
156+
}
157+
158+
// no graphical output and no network access
159+
command := []string{targetQemu, "-nographic", "-nic", "none", "-bios", targetFirmware}
160+
qemuOpts := []string{"-m", targetMem, "-smp", targetNproc}
161+
162+
command = append(command, qemuOpts...)
163+
if targetImage != "" {
164+
command = append(command, targetImage)
165+
}
166+
167+
var logfile *os.File
168+
if targetLogfile != "" {
169+
170+
logfile, err = os.Create(targetLogfile)
171+
if err != nil {
172+
return fmt.Errorf("Could not create Logfile: %w", err)
173+
}
174+
defer logfile.Close()
175+
} else {
176+
logfile, err = os.OpenFile("/dev/null", os.O_WRONLY, fs.ModeDevice)
177+
if err != nil {
178+
return fmt.Errorf("Could not redirect output to '/dev/null': %w", err)
179+
}
180+
defer logfile.Close()
181+
}
182+
183+
gExpect, errchan, err := expect.SpawnWithArgs(
184+
command,
185+
globalTimeout,
186+
expect.Tee(logfile),
187+
expect.CheckDuration(time.Minute),
188+
expect.PartialMatch(false),
189+
expect.SendTimeout(globalTimeout),
190+
)
191+
if err != nil {
192+
return fmt.Errorf("Could not start qemu: %w", err)
193+
}
194+
195+
log.Infof("Started Qemu with command: %v", command)
196+
197+
defer gExpect.Close()
198+
199+
defer func() {
200+
select {
201+
case err = <-errchan:
202+
log.Errorf("Error from Qemu: %v", err)
203+
default:
204+
205+
}
206+
}()
207+
208+
// struct to capture expect and send strings from json.
209+
type expector struct {
210+
Expect string
211+
Send string
212+
Timeout string
213+
}
214+
215+
// loop over all steps and expect/ send the given strings
216+
for _, interaction := range q.steps {
217+
218+
dst := new(expector)
219+
220+
jsString, err := interaction.Expand(target, stepsVars)
221+
if err != nil {
222+
return err
223+
}
224+
225+
interactionParam := test.NewParam(jsString)
226+
js := interactionParam.JSON()
227+
228+
if err := json.Unmarshal(js, dst); err != nil {
229+
return fmt.Errorf("Could not Unmarshal steps: %w", err)
230+
}
231+
232+
// Expect and Send fields must not both be empty
233+
if dst.Expect+dst.Send == "" {
234+
return fmt.Errorf("%s is not a valid step statement", interaction.String())
235+
}
236+
237+
// process expect step
238+
if dst.Expect != "" {
239+
240+
var timeout time.Duration
241+
if dst.Timeout == "" {
242+
timeout = globalTimeout
243+
} else {
244+
if timeout, err = time.ParseDuration(dst.Timeout); err != nil {
245+
return fmt.Errorf("Could not parse timeout '%s' for step: '%s'. %w", dst.Timeout, dst.Expect, err)
246+
}
247+
}
248+
249+
if _, _, err := gExpect.Expect(regexp.MustCompile(dst.Expect), timeout); err != nil {
250+
return fmt.Errorf("Error while expecting '%s': %w", dst.Expect, err)
251+
}
252+
253+
log.Debugf("Completed expect step: '%v' with timeout: %v \n", dst.Expect, timeout.String())
254+
255+
}
256+
257+
// process send step
258+
if dst.Send != "" {
259+
260+
if err := gExpect.Send(dst.Send + "\n"); err != nil {
261+
return fmt.Errorf("Unable to send '%s': %w", dst.Send, err)
262+
}
263+
264+
// notify the user if the timeout field is used incorrectly
265+
if dst.Expect == "" && dst.Timeout != "" {
266+
log.Warnf("The Timeout %v for send step: %v will be ignored.", dst.Timeout, dst.Send)
267+
}
268+
269+
log.Debugf("Completed Send Step: '%v'", dst.Send)
270+
}
271+
}
272+
273+
log.Infof("Matching steps successful")
274+
return nil
275+
}
276+
return teststeps.ForEachTarget(Name, ctx, ch, f)
277+
}

0 commit comments

Comments
 (0)