From 563abfc7520395a8e71e91df1cbdb401ffad77e0 Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Fri, 5 Nov 2021 11:52:14 +0000 Subject: tools/syz-testbed: add a simple web interface For now, just display the avg stats and generate graphs on demand. --- tools/syz-testbed/html.go | 193 +++++++++++++++++++++++++++++++++++++++++++ tools/syz-testbed/stats.go | 13 +++ tools/syz-testbed/testbed.go | 25 +++--- 3 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 tools/syz-testbed/html.go (limited to 'tools') 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(` + + + + {{.Name }} syzkaller + {{HEAD}} + + + +{{define "Table"}} +{{if .}} + + {{range $c := .}} + + {{range $v := $c}} + + {{end}} + + {{end}} +
{{$v}}
+{{end}} +{{end}} + +{{.Name }} syz-testbed +{{template "Table" .Summary}} + +{{range $view := .Views}} +Stat view "{{$view.Name}}"
+Graph over time / +Graph over executions
+{{template "Table" .Table}} +{{end}} + + + +`) 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 { -- cgit mrf-deployment