-
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.
- this is a gather type plugin, holds onto all targets and runs the given command once; returns a framework error if the command fails - add to contest generator and server Signed-off-by: mimir-d <[email protected]>
- Loading branch information
Showing
3 changed files
with
270 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,267 @@ | ||
// Copyright (c) Facebook, Inc. and its affiliates. | ||
// | ||
// This source code is licensed under the MIT license found in the | ||
// LICENSE file in the root directory of this source tree. | ||
|
||
package localsetup | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"os/exec" | ||
"path/filepath" | ||
"syscall" | ||
|
||
"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" | ||
) | ||
|
||
// Name is the name used to look this plugin up. | ||
var Name = "LocalSetup" | ||
|
||
// event names for this plugin | ||
const ( | ||
EventSetupStart = event.Name("LocalSetupStart") | ||
EventSetupDone = event.Name("LocalSetupDone") | ||
) | ||
|
||
// Events defines the events that a TestStep is allow to emit | ||
var Events = []event.Name{ | ||
EventSetupStart, | ||
EventSetupDone, | ||
} | ||
|
||
// eventSetupStartPayload is the payload for EventSetupStart | ||
type eventSetupStartPayload struct { | ||
Path string | ||
Args []string | ||
} | ||
|
||
// eventSetupDonePayload is the payload for EventSetupDone | ||
type eventSetupDonePayload struct { | ||
ExitCode int | ||
Err string | ||
} | ||
|
||
// LocalSetup is used to run arbitrary commands as test steps. | ||
type LocalSetup struct { | ||
// binary to execute; will consult $PATH | ||
binary string | ||
|
||
// arguments to pass to the binary on execution | ||
args []string | ||
} | ||
|
||
// Name returns the plugin name. | ||
func (ts LocalSetup) Name() string { | ||
return Name | ||
} | ||
|
||
func emitEvent( | ||
ctx xcontext.Context, | ||
emitter testevent.Emitter, | ||
target *target.Target, | ||
name event.Name, | ||
payload interface{}, | ||
) error { | ||
jsonstr, err := json.Marshal(payload) | ||
if err != nil { | ||
return fmt.Errorf("cannot encode payload for event '%s': %v", name, err) | ||
} | ||
|
||
jsonPayload := json.RawMessage(jsonstr) | ||
data := testevent.Data{ | ||
EventName: name, | ||
Target: target, | ||
Payload: &jsonPayload, | ||
} | ||
if err := emitter.Emit(ctx, data); err != nil { | ||
return fmt.Errorf("cannot emit event EventCmdStart: %v", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func truncate(in string, maxsize uint) string { | ||
size := uint(len(in)) | ||
if size > maxsize { | ||
size = maxsize | ||
} | ||
return in[:size] | ||
} | ||
|
||
func (ts *LocalSetup) acquireTargets(ctx xcontext.Context, ch test.TestStepChannels) ([]*target.Target, error) { | ||
var targets []*target.Target | ||
|
||
for { | ||
select { | ||
case target, ok := <-ch.In: | ||
if !ok { | ||
ctx.Debugf("acquired %d targets", len(targets)) | ||
return targets, nil | ||
} | ||
targets = append(targets, target) | ||
|
||
case <-ctx.Until(xcontext.ErrPaused): | ||
ctx.Debugf("paused during target acquisition, acquired %d", len(targets)) | ||
return nil, xcontext.ErrPaused | ||
|
||
case <-ctx.Done(): | ||
ctx.Debugf("canceled during target acquisition, acquired %d", len(targets)) | ||
return nil, ctx.Err() | ||
} | ||
} | ||
} | ||
|
||
func (ts *LocalSetup) returnTargets(ctx xcontext.Context, ch test.TestStepChannels, targets []*target.Target) { | ||
for _, target := range targets { | ||
ch.Out <- test.TestStepResult{Target: target} | ||
} | ||
} | ||
|
||
// Run executes the cmd step. | ||
func (ts *LocalSetup) Run(ctx xcontext.Context, ch test.TestStepChannels, params test.TestStepParameters, emitter testevent.Emitter, resumeState json.RawMessage) (json.RawMessage, error) { | ||
log := ctx.Logger() | ||
|
||
if err := ts.setParams(params); err != nil { | ||
return nil, err | ||
} | ||
|
||
// acquire all targets and hold them hostage until the setup is done | ||
targets, err := ts.acquireTargets(ctx, ch) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer ts.returnTargets(ctx, ch, targets) | ||
|
||
// arbitrarily choose first target to associate events with, anyone would work | ||
// but it is unnecessary to have the same event on all targets since this is a | ||
// "gather" type plugin | ||
eventTarget := targets[0] | ||
|
||
// used to manually cancel the exec if step becomes paused | ||
ctx, cancel := xcontext.WithCancel(ctx) | ||
defer cancel() | ||
|
||
go func() { | ||
select { | ||
case <-ctx.Until(xcontext.ErrPaused): | ||
log.Debugf("step was paused, killing the executing command") | ||
cancel() | ||
|
||
case <-ctx.Done(): | ||
// does not leak because cancel is deferred | ||
} | ||
}() | ||
|
||
cmd := exec.CommandContext(ctx, ts.binary, ts.args...) | ||
|
||
var stdout, stderr bytes.Buffer | ||
cmd.Stdout, cmd.Stderr = &stdout, &stderr | ||
cmd.SysProcAttr = &syscall.SysProcAttr{ | ||
// Put the command into a separate session (and group) so signals do not propagate directly to it. | ||
Setsid: true, | ||
} | ||
|
||
log.Debugf("Running command: %+v", cmd) | ||
|
||
err = emitEvent( | ||
ctx, | ||
emitter, eventTarget, | ||
EventSetupStart, | ||
eventSetupStartPayload{Path: cmd.Path, Args: cmd.Args}, | ||
) | ||
if err != nil { | ||
log.Warnf("Failed to emit event: %v", err) | ||
} | ||
|
||
if cmdErr := cmd.Run(); cmdErr != nil { | ||
// the command may have been canceled as a result of the step pausing | ||
if ctx.IsSignaledWith(xcontext.ErrPaused) { | ||
// nothing to save, just continue pausing | ||
return nil, xcontext.ErrPaused | ||
} | ||
|
||
// output the failure event | ||
var payload eventSetupDonePayload | ||
|
||
var exitError *exec.ExitError | ||
if errors.As(cmdErr, &exitError) { | ||
// we ran the binary and it had non-zero exit code | ||
payload = eventSetupDonePayload{ | ||
ExitCode: exitError.ExitCode(), | ||
Err: truncate(fmt.Sprintf("stdout: %s\nstderr: %s", stdout.String(), stderr.String()), 10240), | ||
} | ||
} else { | ||
// something failed while trying to spawn the process | ||
payload = eventSetupDonePayload{ | ||
Err: fmt.Sprintf("unknown error: %v", cmdErr), | ||
} | ||
} | ||
|
||
if err := emitEvent(ctx, emitter, eventTarget, EventSetupDone, payload); err != nil { | ||
log.Warnf("Failed to emit event: %v", err) | ||
} | ||
|
||
// setup failing is actually a framework error, so crash everything | ||
return nil, cmdErr | ||
} | ||
|
||
if err := emitEvent(ctx, emitter, eventTarget, EventSetupDone, nil); err != nil { | ||
log.Warnf("Failed to emit event: %v", err) | ||
} | ||
|
||
// TODO: make step output with stdout/err when PR #83 gets merged | ||
return nil, nil | ||
} | ||
|
||
func (ts *LocalSetup) setParams(params test.TestStepParameters) error { | ||
param := params.GetOne("binary") | ||
if param.IsEmpty() { | ||
return errors.New("invalid or missing 'binary' parameter") | ||
} | ||
|
||
binary := param.String() | ||
if filepath.IsAbs(binary) { | ||
ts.binary = binary | ||
} else { | ||
fullpath, err := exec.LookPath(binary) | ||
if err != nil { | ||
return fmt.Errorf("cannot find '%s' executable in PATH: %v", binary, err) | ||
} | ||
ts.binary = fullpath | ||
} | ||
|
||
ts.args = []string{} | ||
args := params.Get("args") | ||
// expand args in case they use functions, but they shouldnt be target aware | ||
for _, arg := range args { | ||
expanded, err := arg.Expand(&target.Target{}) | ||
if err != nil { | ||
return fmt.Errorf("failed to expand argument: %s -> %v", arg, err) | ||
} | ||
ts.args = append(ts.args, expanded) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// ValidateParameters validates the parameters associated to the TestStep | ||
func (ts *LocalSetup) ValidateParameters(_ xcontext.Context, params test.TestStepParameters) error { | ||
return ts.setParams(params) | ||
} | ||
|
||
// New initializes and returns a new Cmd test step. | ||
func New() test.TestStep { | ||
return &LocalSetup{} | ||
} | ||
|
||
// Load returns the name, factory and events which are needed to register the step. | ||
func Load() (string, test.TestStepFactory, []event.Name) { | ||
return Name, New, Events | ||
} |