aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2021-11-05 11:52:14 +0000
committerAleksandr Nogikh <wp32pw@gmail.com>2021-11-12 16:33:43 +0100
commit563abfc7520395a8e71e91df1cbdb401ffad77e0 (patch)
tree809067ec16c95b0577f92a72ab202ca37df5a736
parentbba04c950dfc1bc6b709232bb32d552223997678 (diff)
tools/syz-testbed: add a simple web interface
For now, just display the avg stats and generate graphs on demand.
-rw-r--r--tools/syz-testbed/html.go193
-rw-r--r--tools/syz-testbed/stats.go13
-rw-r--r--tools/syz-testbed/testbed.go25
3 files changed, 221 insertions, 10 deletions
diff --git a/tools/syz-testbed/html.go b/tools/syz-testbed/html.go
new file mode 100644
index 000000000..fa3786cd4
--- /dev/null
+++ b/tools/syz-testbed/html.go
@@ -0,0 +1,193 @@
+// Copyright 2021 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "sort"
+ "time"
+
+ "github.com/google/syzkaller/pkg/html"
+ "github.com/google/syzkaller/pkg/osutil"
+ "github.com/gorilla/handlers"
+)
+
+func (ctx *TestbedContext) setupHTTPServer() {
+ mux := http.NewServeMux()
+
+ mux.HandleFunc("/", ctx.httpMain)
+ mux.HandleFunc("/graph", ctx.httpGraph)
+
+ listener, err := net.Listen("tcp", ctx.Config.HTTP)
+ if err != nil {
+ log.Fatalf("failed to listen on %s", ctx.Config.HTTP)
+ }
+
+ log.Printf("handling HTTP on %s", listener.Addr())
+ go func() {
+ err := http.Serve(listener, handlers.CompressHandler(mux))
+ if err != nil {
+ log.Fatalf("failed to listen on %v: %v", ctx.Config.HTTP, err)
+ }
+ }()
+}
+
+func (ctx *TestbedContext) httpGraph(w http.ResponseWriter, r *http.Request) {
+ viewName := r.FormValue("view")
+ over := r.FormValue("over")
+
+ if ctx.Config.BenchCmp == "" {
+ http.Error(w, "the path to the benchcmp tool is not specified", http.StatusInternalServerError)
+ return
+ }
+
+ views, err := ctx.GetStatViews()
+ if err != nil {
+ http.Error(w, "failed to retrieve stat views", http.StatusInternalServerError)
+ return
+ }
+ var targetView *StatView
+ for _, view := range views {
+ if view.Name == viewName {
+ targetView = &view
+ break
+ }
+ }
+ if targetView == nil {
+ http.Error(w, "the requested view was not found", http.StatusInternalServerError)
+ return
+ }
+
+ // TODO: move syz-benchcmp functionality to pkg/ and just import it?
+ dir, err := ioutil.TempDir("", "")
+ if err != nil {
+ http.Error(w, "failed to create temp folder", http.StatusInternalServerError)
+ return
+ }
+ defer os.RemoveAll(dir)
+
+ file, err := osutil.TempFile("")
+ if err != nil {
+ http.Error(w, "failed to create temp file", http.StatusInternalServerError)
+ return
+ }
+ defer os.Remove(file)
+
+ benches, err := targetView.SaveAvgBenches(dir)
+ if err != nil {
+ http.Error(w, "failed to save avg benches", http.StatusInternalServerError)
+ return
+ }
+
+ args := append([]string{"-all", "-over", over, "-out", file}, benches...)
+ if out, err := osutil.RunCmd(time.Hour, "", ctx.Config.BenchCmp, args...); err != nil {
+ http.Error(w, "syz-benchcmp failed\n"+string(out), http.StatusInternalServerError)
+ return
+ }
+
+ data, err := ioutil.ReadFile(file)
+ if err != nil {
+ http.Error(w, "failed to read the temporary file", http.StatusInternalServerError)
+ return
+ }
+ w.Write(data)
+}
+
+type uiStatView struct {
+ Name string
+ Table [][]string
+}
+
+type uiMainPage struct {
+ Name string
+ Summary [][]string
+ Views []uiStatView
+}
+
+func (ctx *TestbedContext) httpMain(w http.ResponseWriter, r *http.Request) {
+ uiViews := []uiStatView{}
+ views, err := ctx.GetStatViews()
+ if err != nil {
+ log.Printf("%s", err)
+ views = nil
+ }
+ for _, view := range views {
+ table, err := view.StatsTable()
+ if err != nil {
+ log.Printf("stat table generation failed: %s", err)
+ continue
+ }
+ sort.SliceStable(table, func(i, j int) bool {
+ if len(table[i]) == 0 || len(table[j]) == 0 {
+ return i < j
+ }
+ return table[i][0] < table[j][0]
+ })
+ uiViews = append(uiViews, uiStatView{
+ Name: view.Name,
+ Table: table,
+ })
+ }
+ data := &uiMainPage{
+ Name: ctx.Config.Name,
+ Summary: ctx.TestbedStatsTable(),
+ Views: uiViews,
+ }
+
+ executeTemplate(w, mainTemplate, data)
+}
+
+func executeTemplate(w http.ResponseWriter, templ *template.Template, data interface{}) {
+ buf := new(bytes.Buffer)
+ if err := templ.Execute(buf, data); err != nil {
+ log.Printf("failed to execute template: %v", err)
+ http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
+ return
+ }
+ w.Write(buf.Bytes())
+}
+
+var mainTemplate = html.CreatePage(`
+<!doctype html>
+<html>
+<head>
+ <title>{{.Name }} syzkaller</title>
+ {{HEAD}}
+</head>
+<body>
+
+{{define "Table"}}
+{{if .}}
+<table class="list_table">
+ {{range $c := .}}
+ <tr>
+ {{range $v := $c}}
+ <td>{{$v}}</td>
+ {{end}}
+ </tr>
+ {{end}}
+</table>
+{{end}}
+{{end}}
+
+<b>{{.Name }} syz-testbed</b>
+{{template "Table" .Summary}}
+
+{{range $view := .Views}}
+<b>Stat view "{{$view.Name}}"</b><br />
+<a href="/graph?view={{$view.Name}}&over=fuzzing">Graph over time</a> /
+<a href="/graph?view={{$view.Name}}&over=exec+total">Graph over executions</a> <br />
+{{template "Table" .Table}}
+{{end}}
+
+</body>
+</html>
+`)
diff --git a/tools/syz-testbed/stats.go b/tools/syz-testbed/stats.go
index f244bcbda..3af9d235e 100644
--- a/tools/syz-testbed/stats.go
+++ b/tools/syz-testbed/stats.go
@@ -230,6 +230,19 @@ func (group *RunResultGroup) SaveAvgBenchFile(fileName string) error {
return nil
}
+func (view *StatView) SaveAvgBenches(benchDir string) ([]string, error) {
+ files := []string{}
+ for _, group := range view.Groups {
+ fileName := filepath.Join(benchDir, fmt.Sprintf("avg_%v.txt", group.Name))
+ err := group.SaveAvgBenchFile(fileName)
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, fileName)
+ }
+ return files, nil
+}
+
func SaveTableAsCsv(table [][]string, fileName string) error {
f, err := os.Create(fileName)
if err != nil {
diff --git a/tools/syz-testbed/testbed.go b/tools/syz-testbed/testbed.go
index 819a61b34..e67dd6883 100644
--- a/tools/syz-testbed/testbed.go
+++ b/tools/syz-testbed/testbed.go
@@ -33,6 +33,8 @@ type TestbedConfig struct {
Name string `json:"name"` // name of the testbed
MaxInstances int `json:"max_instances"` // max # of simultaneously running instances
RunTime DurationConfig `json:"run_time"` // lifetime of an instance (default "24h")
+ HTTP string `json:"http"` // on which port to set up a simple web dashboard
+ BenchCmp string `json:"benchcmp"` // path to the syz-benchcmp executable
Corpus string `json:"corpus"` // path to the corpus file
Workdir string `json:"workdir"` // instances will be checked out there
ManagerConfig json.RawMessage `json:"manager_config"` // base manager config
@@ -80,13 +82,16 @@ func main() {
tool.Failf("failed to parse manager config: %s", err)
}
if managerCfg.HTTP == "" {
- managerCfg.HTTP = "0.0.0.0:0"
+ // Actually we don't care much about the specific ports of syz-managers.
+ managerCfg.HTTP = ":0"
}
ctx := TestbedContext{
Config: cfg,
ManagerConfig: managerCfg,
}
+ go ctx.setupHTTPServer()
+
for _, checkoutCfg := range cfg.Checkouts {
co, err := ctx.NewCheckout(&checkoutCfg)
if err != nil {
@@ -158,7 +163,6 @@ func (ctx *TestbedContext) saveStatView(view StatView) error {
"checkout_stats.csv": (StatView).StatsTable,
"instance_stats.csv": (StatView).InstanceStatsTable,
}
-
for fileName, genFunc := range tableStats {
table, err := genFunc(view)
if err == nil {
@@ -167,14 +171,11 @@ func (ctx *TestbedContext) saveStatView(view StatView) error {
log.Printf("some error: %s", err)
}
}
- for _, group := range view.Groups {
- fileName := fmt.Sprintf("avg_%v.txt", group.Name)
- group.SaveAvgBenchFile(filepath.Join(benchDir, fileName))
- }
- return nil
+ _, err = view.SaveAvgBenches(benchDir)
+ return err
}
-func (ctx *TestbedContext) saveTestbedStats(file string) error {
+func (ctx *TestbedContext) TestbedStatsTable() [][]string {
table := [][]string{
{"Checkout", "Running", "Completed", "Until reset"},
}
@@ -190,7 +191,7 @@ func (ctx *TestbedContext) saveTestbedStats(file string) error {
until,
})
}
- return SaveTableAsCsv(table, file)
+ return table
}
func (ctx *TestbedContext) SaveStats() error {
@@ -207,7 +208,8 @@ func (ctx *TestbedContext) SaveStats() error {
return err
}
}
- return ctx.saveTestbedStats(filepath.Join(ctx.Config.Workdir, "testbed.csv"))
+ table := ctx.TestbedStatsTable()
+ return SaveTableAsCsv(table, filepath.Join(ctx.Config.Workdir, "testbed.csv"))
}
func (ctx *TestbedContext) generateInstances(count int) ([]*Instance, error) {
@@ -326,6 +328,9 @@ func checkConfig(cfg *TestbedConfig) error {
if cfg.MaxInstances < 1 {
return fmt.Errorf("max_instances cannot be less than 1")
}
+ if cfg.BenchCmp != "" && !osutil.IsExist(cfg.BenchCmp) {
+ return fmt.Errorf("benchmp path is specified, but %s does not exist", cfg.BenchCmp)
+ }
cfg.Corpus = osutil.Abs(cfg.Corpus)
names := make(map[string]bool)
for idx := range cfg.Checkouts {