Skip to content

Commit

Permalink
Add the dut command from linuxboot
Browse files Browse the repository at this point in the history
It is much easier to bring dut into cpu than have 2 repos.
Further, dut is not a combined cpu and cpud; it is useful
to have this illustrative use of the cpu package.

Signed-off-by: Ronald G. Minnich <[email protected]>
  • Loading branch information
rminnich committed Aug 2, 2022
1 parent de61a4b commit fd1ab7f
Show file tree
Hide file tree
Showing 6 changed files with 514 additions and 0 deletions.
66 changes: 66 additions & 0 deletions uinit/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
// 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
// it exits. Flags for this service:
// pubkey: name of the public key file
// 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 uinit/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")
host = flag.String("host", "192.168.0.1", "hostname")
klog = flag.Bool("klog", false, "Direct all logging to klog -- depends on debug")
port = flag.String("port", "8080", "port number")
dir = flag.String("dir", ".", "directory to serve")

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

func dutStart(t, host, port string) (net.Listener, error) {
ln, err := net.Listen(t, host+":"+port)
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(host, port string) error {
l, err := dutStart("tcp", host, port)
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(host, port, pubkey, hostkey, cpuport string) error {
var req = &RPCCPU{Port: cpuport}
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("tcp", host, port)
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(*host, *port)
case "cpu":
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(*host, *port, *pubKey, *hostKey, *cpuPort); err != nil {
log.Printf("cpu service: %v", err)
}
case "device":
err = uinit(*host, *port)
// 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 uinit/dut_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"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{}},
{c: "Kexec", r: RPCKexec{When: 5 * time.Second}, err: "Not yet"},
}
l, err := dutStart("tcp", "localhost", "")
if err != nil {
t.Fatal(err)
}

a := l.Addr()
t.Logf("listening on %v", a)
// Kick off our node.
go func() {
time.Sleep(1)
if err := uinit(a.Network(), a.String(), "17010"); err != nil {
t.Fatalf("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)
}
})
}
}
79 changes: 79 additions & 0 deletions uinit/rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 {
PubKey []byte
HostKey []byte
Port string
}

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

0 comments on commit fd1ab7f

Please sign in to comment.