Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the dut command from linuxboot #125

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions cmds/dut/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// dut manages Devices Under Test (a.k.a. DUT) from a host.
// A primary goal is allowing multiple hosts with any architecture to connect.
//
// This program was designed to be used in u-root images, as the uinit,
// or in other initramfs systems. It can not function as a standalone
// init: it assumes network is set up, for example.
//
// In this document, dut refers to this program, and DUT refers to
// Devices Under Test. Hopefully this is not too confusing, but it is
// convenient. Also, please note: DUT is plural (Devices). We don't need
// to say DUTs -- at least one is assumed.
//
// The same dut binary runs on host and DUT, in either device mode (i.e.
// on the DUT), or in some host-specific mode. The mode is chosen by
// the first non-flag argument. If there are flags specific to that mode,
// they follow that argument.
// E.g., when uinit is run on the host and we want it to enable cpu daemons
// on the DUT, we run it as follows:
// dut cpu -key ...
// the -key switch is only valid following the cpu mode argument.
//
// modes
// dut currently supports 3 modes.
//
// The first, default, mode, is "device". In device mode, dut makes an http connection
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this "In device mode, DUT makes" vs dut makes? or dut on DUT makes?

also, should the DUT start the RPC server before calling back to the host? if you do it as written, there's a window of time where the host dut could call back to the DUT, but the service isn't ready yet. if you time it poorly, you'll get stuck in tcp limbo or something.

// to a dut running on a host, then starts an HTTP RPC server.
//
// The second mode is "tester". In this mode, dut calls the Welcome service, followed
// by the Reboot service. Tester can be useful, run by a shell script in a for loop, for
// ensure reboot is reliable.
//
// The third mode is "cpu". dut will direct the DUT to start a cpu service, and block until
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding "block until" - i might want the host dut command to return once the cpud service is running. i.e. "the CPU RPC is complete when i know the server is running", and not "when the server has exited".

this will help me with vmapp, where i can block until i know cpud is fully listening.

// it exits. Flags for this service:
// pubkey: name of the public key file
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a nice Go way to use the same parameters as the cpud command? e.g. right now cpud has pubkey == -pk. and of course -net for vsock.

not sure if we can have a package struct that is the expected configuration, etc, so that we don't have two copies of all the args and parsing that cpud expects. as an added bonus, we wouldn't have different names for the same arg either.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have common flags in the client/ package? probably.

// hostkey: name of the host key file
// cpuport: port on which to serve the cpu service
//
// Theory of Operation
// dut runs on the host, accepting connections from DUT, and controlling them via
// Go HTTP RPC commands. As each command is executed, its response is printed.
// Commands are:
//
// Welcome -- get a welcome message
// Argument: None
// Return: a welcome message in cowsay format:
// < welcome to DUT >
// --------------
// \ ^__^
// \ (oo)\_______
// (__)\ )\/\
// ||----w |
// || ||
//
// Die -- force dut on DUT to exit
// Argument: time to sleep before exiting as a time.Duration
// Return: no return; kills the program running on DUT
//
// Reboot
// Argument: time to sleep before rebooting as a time.Duration
//
// CPU -- Start a CPU server on DUT
// Arguments: public key and host key as a []byte, service port as a string
// Returns: returns (possibly nil) error exit value of cpu server; blocks until it is done
//
//
package main
189 changes: 189 additions & 0 deletions cmds/dut/dut.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// This is a very simple dut program. It builds into one binary to implement
// both client and server. It's just easier to see both sides of the code and test
// that way.
package main

import (
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/rpc"
"os"
"time"

"github.com/u-root/u-root/pkg/ulog"
"golang.org/x/sys/unix"
)

var (
debug = flag.Bool("d", false, "Enable debug prints")
addr = flag.String("addr", "192.168.0.1:8080", "DUT addr in addr:port format")
network = flag.String("net", "tcp", "Network to use")
klog = flag.Bool("klog", false, "Direct all logging to klog -- depends on debug")

// for debug
v = func(string, ...interface{}) {}
)

func dutStart(network, addr string) (net.Listener, error) {
ln, err := net.Listen(network, addr)
if err != nil {
log.Print(err)
return nil, err
}
log.Printf("Listening on %v at %v", ln.Addr(), time.Now())
return ln, nil
}

func dutAccept(l net.Listener) (net.Conn, error) {
if err := l.(*net.TCPListener).SetDeadline(time.Now().Add(3 * time.Minute)); err != nil {
return nil, err
}
c, err := l.Accept()
if err != nil {
log.Printf("Listen failed: %v at %v", err, time.Now())
log.Print(err)
return nil, err
}
log.Printf("Accepted %v", c)
return c, nil
}

func dutRPC(network, addr string) error {
l, err := dutStart(network, addr)
if err != nil {
return err
}
c, err := dutAccept(l)
if err != nil {
return err
}
cl := rpc.NewClient(c)
for _, cmd := range []struct {
call string
args interface{}
}{
{"Command.Welcome", &RPCWelcome{}},
{"Command.Reboot", &RPCReboot{}},
} {
var r RPCRes
if err := cl.Call(cmd.call, cmd.args, &r); err != nil {
return err
}
fmt.Printf("%v(%v): %v\n", cmd.call, cmd.args, string(r.C))
}

if c, err = dutAccept(l); err != nil {
return err
}
cl = rpc.NewClient(c)
var r RPCRes
if err := cl.Call("Command.Welcome", &RPCWelcome{}, &r); err != nil {
return err
}
fmt.Printf("%v(%v): %v\n", "Command.Welcome", nil, string(r.C))

return nil
}

func dutcpu(network, addr, pubkey, hostkey, cpuport string) error {
var req = &RPCCPU{Network: network, Addr: addr}
var err error

// we send the pubkey and hostkey as the value of the key, not the
// name of the file.
// TODO: maybe use ssh_config to find keys? the cpu client can do that.
// Note: the public key is not optional. That said, we do not test
// for len(*pubKey) > 0; if it is set to ""< ReadFile will return
// an error.
if req.PubKey, err = ioutil.ReadFile(pubkey); err != nil {
return fmt.Errorf("Reading pubKey:%w", err)
}
if len(hostkey) > 0 {
if req.HostKey, err = ioutil.ReadFile(hostkey); err != nil {
return fmt.Errorf("Reading hostKey:%w", err)
}
}

l, err := dutStart(network, addr)
if err != nil {
return err
}

c, err := dutAccept(l)
if err != nil {
return err
}

cl := rpc.NewClient(c)

for _, cmd := range []struct {
call string
args interface{}
}{
{"Command.Welcome", &RPCWelcome{}},
{"Command.Welcome", &RPCWelcome{}},
{"Command.CPU", req},
} {
var r RPCRes
if err := cl.Call(cmd.call, cmd.args, &r); err != nil {
return err
}
fmt.Printf("%v(%v): %v\n", cmd.call, cmd.args, string(r.C))
}
return err
}

func main() {
// for CPU
flag.Parse()

if *debug {
v = log.Printf
if *klog {
ulog.KernelLog.Reinit()
v = ulog.KernelLog.Printf
}
}
a := flag.Args()
if len(a) == 0 {
a = []string{"device"}
}

os.Args = a
var err error
v("Mode is %v", a[0])
switch a[0] {
case "tester":
err = dutRPC(*network, *addr)
case "cpu":
// These flags are separated out as the only have meaning for the "cpu" client option
var (
pubKey = flag.String("pubkey", "key.pub", "public key file")
hostKey = flag.String("hostkey", "", "host key file -- usually empty")
cpuPort = flag.String("cpuport", "17010", "cpu port -- IANA value is ncpu tcp/17010")
)
v("Parse %v", os.Args)
flag.Parse()
v("pubkey %v", *pubKey)
if err := dutcpu(*network, *addr, *pubKey, *hostKey, *cpuPort); err != nil {
log.Printf("cpu service: %v", err)
}
case "device":
err = uinit(*network, *addr)
// What to do after a return? Reboot I suppose.
log.Printf("Device returns with error %v", err)
if err := unix.Reboot(int(unix.LINUX_REBOOT_CMD_RESTART)); err != nil {
log.Printf("Reboot failed, not sure what to do now.")
}
default:
log.Printf("Unknown mode %v", a[0])
}
log.Printf("We are now done ......................")
if err != nil {
log.Printf("%v", err)
os.Exit(2)
}
}
52 changes: 52 additions & 0 deletions cmds/dut/dut_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"log"
"net/rpc"
"testing"
"time"
)

func TestUinit(t *testing.T) {
var tests = []struct {
c string
r interface{}
err string
}{
{c: "Welcome", r: RPCWelcome{}},
{c: "Reboot", r: RPCReboot{}},
}
l, err := dutStart("tcp", ":")
if err != nil {
t.Fatal(err)
}

a := l.Addr()
t.Logf("listening on %v", a)
// Kick off our node.
go func() {
time.Sleep(1 * time.Second)
if err := uinit(a.Network(), a.String()); err != nil {
log.Printf("starting uinit: got %v, want nil", err)
}
}()

c, err := dutAccept(l)
if err != nil {
t.Fatal(err)
}
t.Logf("Connected on %v", c)

cl := rpc.NewClient(c)
for _, tt := range tests {
t.Run(tt.c, func(t *testing.T) {
var r RPCRes
if err = cl.Call("Command."+tt.c, tt.r, &r); err != nil {
t.Fatalf("Call to %v: got %v, want nil", tt.c, err)
}
if r.Err != tt.err {
t.Errorf("%v: got %v, want %v", tt, r.Err, tt.err)
}
})
}
}
80 changes: 80 additions & 0 deletions cmds/dut/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"fmt"
"log"
"os"
"time"

"golang.org/x/sys/unix"
)

type RPCRes struct {
C []byte
Err string
}

type Command int

type RPCWelcome struct {
}

func (*Command) Welcome(args *RPCWelcome, r *RPCRes) error {
r.C = []byte(welcome)
r.Err = ""
log.Printf("welcome")
return nil
}

type RPCExit struct {
When time.Duration
}

func (*Command) Die(args *RPCExit, r *RPCRes) error {
go func() {
time.Sleep(args.When)
log.Printf("die exits")
os.Exit(0)
}()
*r = RPCRes{}
log.Printf("die returns")
return nil
}

type RPCReboot struct {
When time.Duration
}

func (*Command) Reboot(args *RPCReboot, r *RPCRes) error {
go func() {
time.Sleep(args.When)
if err := unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART); err != nil {
log.Printf("%v\n", err)
}
}()
*r = RPCRes{}
log.Printf("reboot returns")
return nil
}

type RPCCPU struct {
Network string
Addr string
PubKey []byte
HostKey []byte
}

func (*Command) CPU(args *RPCCPU, r *RPCRes) error {
v("CPU")
res := make(chan error)
go func(network, addr string, pubKey, hostKey []byte) {
v("cpu serve(%q, %q,%q,%q)", network, addr, pubKey, hostKey)
err := serve(network, addr, pubKey, hostKey)
v("cpu serve returns")
res <- err
}(args.Network, args.Addr, args.PubKey, args.HostKey)
err := <-res
*r = RPCRes{Err: fmt.Sprintf("%v", err)}
v("cpud returns")
return nil
}
Loading