diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2019-02-14 10:35:03 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2019-02-17 15:08:45 +0100 |
| commit | 3e98cc30803fb5e41504dd08b1325cb074a8a3f2 (patch) | |
| tree | 5f0a7a4702bedbb9706c158e09e4be1875894625 /dashboard | |
| parent | f42dee6d5e501a061cdbb807672361369bf28492 (diff) | |
dashboard/app: poll commits info
This implements 2 features:
- syz-ci polls a set of additional repos to discover fixing commits sooner
(e.g. it can now discover a fixing commit in netfilter tree before
it reaches any of the tested trees).
- syz-ci uploads info about commits to dashboard.
For example, a user marks a bug as fixed by commit "foo: bar".
syz-ci will find this commit in the main namespace repo
and upload commmit hash/date/author to dashboard. This in turn
allows to show links to fixing commits.
Fixes #691
Fixes #610
Diffstat (limited to 'dashboard')
| -rw-r--r-- | dashboard/app/aetest.go | 8 | ||||
| -rw-r--r-- | dashboard/app/api.go | 234 | ||||
| -rw-r--r-- | dashboard/app/app_test.go | 49 | ||||
| -rw-r--r-- | dashboard/app/bug.html | 8 | ||||
| -rw-r--r-- | dashboard/app/commit_poll_test.go | 94 | ||||
| -rw-r--r-- | dashboard/app/config.go | 66 | ||||
| -rw-r--r-- | dashboard/app/email_test.go | 2 | ||||
| -rw-r--r-- | dashboard/app/entities.go | 45 | ||||
| -rw-r--r-- | dashboard/app/fix_test.go | 25 | ||||
| -rw-r--r-- | dashboard/app/index.yaml | 6 | ||||
| -rw-r--r-- | dashboard/app/main.go | 54 | ||||
| -rw-r--r-- | dashboard/app/noaetest.go | 8 | ||||
| -rw-r--r-- | dashboard/app/reporting.go | 4 | ||||
| -rw-r--r-- | dashboard/app/static/style.css | 13 | ||||
| -rw-r--r-- | dashboard/app/templates.html | 26 | ||||
| -rw-r--r-- | dashboard/dashapi/dashapi.go | 39 |
16 files changed, 549 insertions, 132 deletions
diff --git a/dashboard/app/aetest.go b/dashboard/app/aetest.go new file mode 100644 index 000000000..c94379bc6 --- /dev/null +++ b/dashboard/app/aetest.go @@ -0,0 +1,8 @@ +// Copyright 2019 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. + +// +build aetest + +package dash + +const isAppEngineTest = true diff --git a/dashboard/app/api.go b/dashboard/app/api.go index 4f5b68be7..c37afb404 100644 --- a/dashboard/app/api.go +++ b/dashboard/app/api.go @@ -46,6 +46,8 @@ var apiNamespaceHandlers = map[string]APINamespaceHandler{ "report_failed_repro": apiReportFailedRepro, "need_repro": apiNeedRepro, "manager_stats": apiManagerStats, + "commit_poll": apiCommitPoll, + "upload_commits": apiUploadCommits, } type JSONHandler func(c context.Context, r *http.Request) (interface{}, error) @@ -201,20 +203,153 @@ loop: commits = append(commits, com) } sort.Strings(commits) - reportEmail := "" + resp := &dashapi.BuilderPollResp{ + PendingCommits: commits, + ReportEmail: reportEmail(c, ns), + } + return resp, nil +} + +func reportEmail(c context.Context, ns string) string { for _, reporting := range config.Namespaces[ns].Reporting { if _, ok := reporting.Config.(*EmailConfig); ok { - reportEmail = ownEmail(c) - break + return ownEmail(c) } } - resp := &dashapi.BuilderPollResp{ - PendingCommits: commits, - ReportEmail: reportEmail, + return "" +} + +func apiCommitPoll(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { + resp := &dashapi.CommitPollResp{ + ReportEmail: reportEmail(c, ns), + } + for _, repo := range config.Namespaces[ns].Repos { + resp.Repos = append(resp.Repos, dashapi.Repo{ + URL: repo.URL, + Branch: repo.Branch, + }) + } + var bugs []*Bug + _, err := datastore.NewQuery("Bug"). + Filter("Namespace=", ns). + Filter("NeedCommitInfo=", true). + Project("Commits"). + Limit(100). + GetAll(c, &bugs) + if err != nil { + return nil, fmt.Errorf("failed to query bugs: %v", err) + } + commits := make(map[string]bool) + for _, bug := range bugs { + for _, com := range bug.Commits { + commits[com] = true + } + } + for com := range commits { + resp.Commits = append(resp.Commits, com) } return resp, nil } +func apiUploadCommits(c context.Context, ns string, r *http.Request, payload []byte) (interface{}, error) { + req := new(dashapi.CommitPollResultReq) + if err := json.Unmarshal(payload, req); err != nil { + return nil, fmt.Errorf("failed to unmarshal request: %v", err) + } + // This adds fixing commits to bugs. + err := addCommitsToBugs(c, ns, "", nil, req.Commits) + if err != nil { + return nil, err + } + // Now add commit info to commits. + for _, com := range req.Commits { + if com.Hash == "" { + continue + } + if err := addCommitInfo(c, ns, com); err != nil { + return nil, err + } + } + return nil, nil +} + +func addCommitInfo(c context.Context, ns string, com dashapi.Commit) error { + var bugs []*Bug + keys, err := datastore.NewQuery("Bug"). + Filter("Namespace=", ns). + Filter("Commits=", com.Title). + GetAll(c, &bugs) + if err != nil { + return fmt.Errorf("failed to query bugs: %v", err) + } + for i, bug := range bugs { + if err := addCommitInfoToBug(c, bug, keys[i], com); err != nil { + return err + } + } + return nil +} + +func addCommitInfoToBug(c context.Context, bug *Bug, bugKey *datastore.Key, com dashapi.Commit) error { + if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil { + return err + } else if !needUpdate { + return nil + } + tx := func(c context.Context) error { + bug := new(Bug) + if err := datastore.Get(c, bugKey, bug); err != nil { + return fmt.Errorf("failed to get bug %v: %v", bugKey.StringID(), err) + } + if needUpdate, err := addCommitInfoToBugImpl(c, bug, com); err != nil { + return err + } else if !needUpdate { + return nil + } + if _, err := datastore.Put(c, bugKey, bug); err != nil { + return fmt.Errorf("failed to put bug: %v", err) + } + return nil + } + return datastore.RunInTransaction(c, tx, nil) +} + +func addCommitInfoToBugImpl(c context.Context, bug *Bug, com dashapi.Commit) (bool, error) { + ci := -1 + for i, title := range bug.Commits { + if title == com.Title { + ci = i + break + } + } + if ci < 0 { + return false, nil + } + for len(bug.CommitInfo) < len(bug.Commits) { + bug.CommitInfo = append(bug.CommitInfo, CommitInfo{}) + } + hash0 := bug.CommitInfo[ci].Hash + date0 := bug.CommitInfo[ci].Date + author0 := bug.CommitInfo[ci].Author + needCommitInfo0 := bug.NeedCommitInfo + + bug.CommitInfo[ci].Hash = com.Hash + bug.CommitInfo[ci].Date = com.Date + bug.CommitInfo[ci].Author = com.Author + bug.NeedCommitInfo = false + for i := range bug.CommitInfo { + if bug.CommitInfo[i].Hash == "" { + bug.NeedCommitInfo = true + break + } + } + changed := hash0 != bug.CommitInfo[ci].Hash || + date0 != bug.CommitInfo[ci].Date || + author0 != bug.CommitInfo[ci].Author || + needCommitInfo0 != bug.NeedCommitInfo + return changed, nil +} + func apiJobPoll(c context.Context, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.JobPollReq) if err := json.Unmarshal(payload, req); err != nil { @@ -255,6 +390,11 @@ func apiUploadBuild(c context.Context, ns string, r *http.Request, payload []byt } } if len(req.Commits) != 0 || len(req.FixCommits) != 0 { + for i := range req.FixCommits { + // Reset hashes just to make sure, + // the build does not necessary come from the master repo, so we must not remember hashes. + req.FixCommits[i].Hash = "" + } if err := addCommitsToBugs(c, ns, req.Manager, req.Commits, req.FixCommits); err != nil { // We've already uploaded the build successfully and manager can use it. // Moreover, addCommitsToBugs scans all bugs and can take long time. @@ -329,8 +469,7 @@ func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build return build, true, nil } -func addCommitsToBugs(c context.Context, ns, manager string, - titles []string, fixCommits []dashapi.FixCommit) error { +func addCommitsToBugs(c context.Context, ns, manager string, titles []string, fixCommits []dashapi.Commit) error { presentCommits := make(map[string]bool) bugFixedBy := make(map[string][]string) for _, com := range titles { @@ -338,7 +477,9 @@ func addCommitsToBugs(c context.Context, ns, manager string, } for _, com := range fixCommits { presentCommits[com.Title] = true - bugFixedBy[com.BugID] = append(bugFixedBy[com.BugID], com.Title) + for _, bugID := range com.BugIDs { + bugFixedBy[bugID] = append(bugFixedBy[bugID], com.Title) + } } managers, err := managerList(c, ns) if err != nil { @@ -347,10 +488,9 @@ func addCommitsToBugs(c context.Context, ns, manager string, // Fetching all bugs in a namespace can be slow, and there is no way to filter only Open/Dup statuses. // So we run a separate query for each status, this both avoids fetching unnecessary data // and splits a long query into two (two smaller queries have lower chances of trigerring - // timeouts that one huge). + // timeouts than one huge). for _, status := range []int{BugStatusOpen, BugStatusDup} { - err := addCommitsToBugsInStatus(c, status, ns, manager, managers, - fixCommits, presentCommits, bugFixedBy) + err := addCommitsToBugsInStatus(c, status, ns, manager, managers, presentCommits, bugFixedBy) if err != nil { return err } @@ -359,7 +499,7 @@ func addCommitsToBugs(c context.Context, ns, manager string, } func addCommitsToBugsInStatus(c context.Context, status int, ns, manager string, managers []string, - fixCommits []dashapi.FixCommit, presentCommits map[string]bool, bugFixedBy map[string][]string) error { + presentCommits map[string]bool, bugFixedBy map[string][]string) error { var bugs []*Bug _, err := datastore.NewQuery("Bug"). Filter("Namespace=", ns). @@ -409,22 +549,22 @@ func addCommitsToBug(c context.Context, bug *Bug, manager string, managers []str return nil } if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) { - bug.Commits = fixCommits - bug.FixTime = now - bug.PatchedOn = nil - } - bug.PatchedOn = append(bug.PatchedOn, manager) - if bug.Status == BugStatusOpen { - fixed := true - for _, mgr := range managers { - if !stringInList(bug.PatchedOn, mgr) { - fixed = false - break + bug.updateCommits(fixCommits, now) + } + if manager != "" { + bug.PatchedOn = append(bug.PatchedOn, manager) + if bug.Status == BugStatusOpen { + fixed := true + for _, mgr := range managers { + if !stringInList(bug.PatchedOn, mgr) { + fixed = false + break + } + } + if fixed { + bug.Status = BugStatusFixed + bug.Closed = now } - } - if fixed { - bug.Status = BugStatusFixed - bug.Closed = now } } if _, err := datastore.Put(c, bugKey, bug); err != nil { @@ -435,6 +575,25 @@ func addCommitsToBug(c context.Context, bug *Bug, manager string, managers []str return datastore.RunInTransaction(c, tx, nil) } +func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string, + presentCommits map[string]bool, dolog bool) bool { + if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) { + if dolog { + log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits) + } + return true + } + if len(bug.Commits) == 0 || manager == "" || stringInList(bug.PatchedOn, manager) { + return false + } + for _, com := range bug.Commits { + if !presentCommits[com] { + return false + } + } + return true +} + func managerList(c context.Context, ns string) ([]string, error) { var builds []*Build _, err := datastore.NewQuery("Build"). @@ -456,25 +615,6 @@ func managerList(c context.Context, ns string) ([]string, error) { return managers, nil } -func bugNeedsCommitUpdate(c context.Context, bug *Bug, manager string, fixCommits []string, - presentCommits map[string]bool, dolog bool) bool { - if len(fixCommits) != 0 && !reflect.DeepEqual(bug.Commits, fixCommits) { - if dolog { - log.Infof(c, "bug %q is fixed with %q", bug.Title, fixCommits) - } - return true - } - if len(bug.Commits) == 0 || stringInList(bug.PatchedOn, manager) { - return false - } - for _, com := range bug.Commits { - if !presentCommits[com] { - return false - } - } - return true -} - func stringInList(list []string, str string) bool { for _, s := range list { if s == str { diff --git a/dashboard/app/app_test.go b/dashboard/app/app_test.go index e030ddaa4..a63f6e8d9 100644 --- a/dashboard/app/app_test.go +++ b/dashboard/app/app_test.go @@ -37,6 +37,20 @@ var testConfig = &GlobalConfig{ Clients: map[string]string{ client1: key1, }, + Repos: []KernelRepo{ + { + URL: "git://syzkaller.org", + Branch: "branch10", + Alias: "repo10alias", + CC: []string{"maintainers@repo10.org", "bugs@repo10.org"}, + }, + { + URL: "git://github.com/google/syzkaller", + Branch: "master", + Alias: "repo10alias", + CC: []string{"maintainers@repo10.org", "bugs@repo10.org"}, + }, + }, Reporting: []Reporting{ { Name: "reporting1", @@ -67,6 +81,14 @@ var testConfig = &GlobalConfig{ Clients: map[string]string{ client2: key2, }, + Repos: []KernelRepo{ + { + URL: "git://syzkaller.org", + Branch: "branch10", + Alias: "repo10alias", + CC: []string{"maintainers@repo10.org", "bugs@repo10.org"}, + }, + }, Managers: map[string]ConfigManager{ "restricted-manager": { RestrictedTestingRepo: "git://restricted.git/restricted.git", @@ -109,6 +131,13 @@ var testConfig = &GlobalConfig{ Clients: map[string]string{ clientAdmin: keyAdmin, }, + Repos: []KernelRepo{ + { + URL: "git://syzkaller.org/access-admin.git", + Branch: "access-admin", + Alias: "access-admin", + }, + }, Reporting: []Reporting{ { Name: "access-admin-reporting1", @@ -126,6 +155,13 @@ var testConfig = &GlobalConfig{ Clients: map[string]string{ clientUser: keyUser, }, + Repos: []KernelRepo{ + { + URL: "git://syzkaller.org/access-user.git", + Branch: "access-user", + Alias: "access-user", + }, + }, Reporting: []Reporting{ { AccessLevel: AccessAdmin, @@ -144,6 +180,13 @@ var testConfig = &GlobalConfig{ Clients: map[string]string{ clientPublic: keyPublic, }, + Repos: []KernelRepo{ + { + URL: "git://syzkaller.org/access-public.git", + Branch: "access-public", + Alias: "access-public", + }, + }, Reporting: []Reporting{ { AccessLevel: AccessUser, @@ -157,12 +200,6 @@ var testConfig = &GlobalConfig{ }, }, }, - KernelRepos: map[string]KernelRepo{ - "repo10/branch10": { - Alias: "repo10alias", - CC: []string{"maintainers@repo10.org", "bugs@repo10.org"}, - }, - }, } const ( diff --git a/dashboard/app/bug.html b/dashboard/app/bug.html index fa59f14bc..8b3afaf3d 100644 --- a/dashboard/app/bug.html +++ b/dashboard/app/bug.html @@ -18,12 +18,12 @@ Page with details about a single bug. Status: {{if .Bug.ExternalLink}}<a href="{{.Bug.ExternalLink}}">{{.Bug.Status}}</a>{{else}}{{.Bug.Status}}{{end}}<br> Reported-by: {{.Bug.CreditEmail}}<br> {{if .Bug.Commits}} - Commits: {{.Bug.Commits}}<br> + Fix commit: {{template "fix_commits" .Bug.Commits}}<br> {{if .Bug.ClosedTime.IsZero}} Patched on: {{.Bug.PatchedOn}}, missing on: {{.Bug.MissingOn}}<br> {{end}} {{end}} - First: {{formatLateness $.Now $.Bug.FirstTime}}, last: {{formatLateness $.Now $.Bug.LastTime}}<br> + First crash: {{formatLateness $.Now $.Bug.FirstTime}}, last: {{formatLateness $.Now $.Bug.LastTime}}<br> {{template "bug_list" .DupOf}} {{template "bug_list" .Dups}} @@ -57,8 +57,8 @@ Page with details about a single bug. <td class="time">{{formatTime $c.Time}}</td> <td class="kernel" title="{{$c.KernelAlias}}">{{$c.KernelAlias}}</td> <td class="tag" title="{{$c.KernelCommit}} -{{formatTime $c.KernelCommitDate}}">{{formatShortHash $c.KernelCommit}}</td> - <td class="tag" title="{{$c.SyzkallerCommit}}">{{formatShortHash $c.SyzkallerCommit}}</td> +{{formatTime $c.KernelCommitDate}}">{{link $c.KernelCommitLink (formatShortHash $c.KernelCommit)}}</td> + <td class="tag">{{link $c.SyzkallerCommitLink (formatShortHash $c.SyzkallerCommit)}}</td> <td class="config">{{if $c.KernelConfigLink}}<a href="{{$c.KernelConfigLink}}">.config</a>{{end}}</td> <td class="repro">{{if $c.LogLink}}<a href="{{$c.LogLink}}">log</a>{{end}}</td> <td class="repro">{{if $c.ReportLink}}<a href="{{$c.ReportLink}}">report</a>{{end}}</td> diff --git a/dashboard/app/commit_poll_test.go b/dashboard/app/commit_poll_test.go new file mode 100644 index 000000000..48c86292f --- /dev/null +++ b/dashboard/app/commit_poll_test.go @@ -0,0 +1,94 @@ +// Copyright 2019 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. + +// +build aetest + +package dash + +import ( + "sort" + "testing" + + "github.com/google/syzkaller/dashboard/dashapi" +) + +func TestCommitPoll(t *testing.T) { + c := NewCtx(t) + defer c.Close() + + build1 := testBuild(1) + c.client.UploadBuild(build1) + + crash1 := testCrash(build1, 1) + c.client.ReportCrash(crash1) + rep1 := c.client.pollBug() + + crash2 := testCrash(build1, 2) + c.client.ReportCrash(crash2) + rep2 := c.client.pollBug() + + // No commits in commit poll. + commitPollResp, err := c.client.CommitPoll() + c.expectOK(err) + c.expectEQ(len(commitPollResp.Repos), 2) + c.expectEQ(commitPollResp.Repos[0].URL, testConfig.Namespaces["test1"].Repos[0].URL) + c.expectEQ(commitPollResp.Repos[0].Branch, testConfig.Namespaces["test1"].Repos[0].Branch) + c.expectEQ(commitPollResp.Repos[1].URL, testConfig.Namespaces["test1"].Repos[1].URL) + c.expectEQ(commitPollResp.Repos[1].Branch, testConfig.Namespaces["test1"].Repos[1].Branch) + c.expectEQ(len(commitPollResp.Commits), 0) + + // Specify fixing commit for the bug. + reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{ + ID: rep1.ID, + Status: dashapi.BugStatusOpen, + FixCommits: []string{"foo: fix1", "foo: fix2"}, + }) + c.expectEQ(reply.OK, true) + + // The commit should appear in commit poll. + for i := 0; i < 2; i++ { + commitPollResp, err = c.client.CommitPoll() + c.expectOK(err) + c.expectEQ(len(commitPollResp.Commits), 2) + sort.Strings(commitPollResp.Commits) + c.expectEQ(commitPollResp.Commits[0], "foo: fix1") + c.expectEQ(commitPollResp.Commits[1], "foo: fix2") + } + + // Upload hash for the first commit and fixing commit for the second bug. + c.expectOK(c.client.UploadCommits([]dashapi.Commit{ + {Hash: "hash1", Title: "foo: fix1"}, + {Hash: "hash2", Title: "bar: fix3", BugIDs: []string{rep2.ID}}, + {Hash: "hash3", Title: "some unrelated commit", BugIDs: []string{"does not exist"}}, + {Hash: "hash4", Title: "another unrelated commit"}, + })) + + commitPollResp, err = c.client.CommitPoll() + c.expectOK(err) + c.expectEQ(len(commitPollResp.Commits), 2) + sort.Strings(commitPollResp.Commits) + c.expectEQ(commitPollResp.Commits[0], "foo: fix1") + c.expectEQ(commitPollResp.Commits[1], "foo: fix2") + + // Upload hash for the second commit and a new fixing commit for the second bug. + c.expectOK(c.client.UploadCommits([]dashapi.Commit{ + {Hash: "hash5", Title: "foo: fix2"}, + {Title: "bar: fix4", BugIDs: []string{rep2.ID}}, + })) + + commitPollResp, err = c.client.CommitPoll() + c.expectOK(err) + c.expectEQ(len(commitPollResp.Commits), 1) + c.expectEQ(commitPollResp.Commits[0], "bar: fix4") + + // Upload hash for the second commit and a new fixing commit for the second bug. + c.expectOK(c.client.UploadCommits([]dashapi.Commit{ + {Hash: "hash1", Title: "foo: fix1"}, + {Hash: "hash5", Title: "foo: fix2"}, + {Hash: "hash6", Title: "bar: fix4"}, + })) + + commitPollResp, err = c.client.CommitPoll() + c.expectOK(err) + c.expectEQ(len(commitPollResp.Commits), 0) +} diff --git a/dashboard/app/config.go b/dashboard/app/config.go index 26487ae6b..f0f3d168d 100644 --- a/dashboard/app/config.go +++ b/dashboard/app/config.go @@ -12,6 +12,7 @@ import ( "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/email" + "github.com/google/syzkaller/pkg/vcs" ) // There are multiple configurable aspects of the app (namespaces, reporting, API clients, etc). @@ -38,8 +39,6 @@ type GlobalConfig struct { // Each namespace has own reporting config, own API clients // and bugs are not merged across namespaces. Namespaces map[string]*Config - // Maps full repository address/branch to description of this repo. - KernelRepos map[string]KernelRepo } // Per-namespace config. @@ -70,6 +69,11 @@ type Config struct { TransformCrash func(build *Build, crash *dashapi.Crash) bool // NeedRepro hook can be used to prevent reproduction of some bugs. NeedRepro func(bug *Bug) bool + // List of kernel repositories for this namespace. + // The first repo considered the "main" repo (e.g. fixing commit info is shown against this repo). + // Other repos are secondary repos, they may be tested or not. + // If not tested they are used to poll for fixing commits. + Repos []KernelRepo } // ConfigManager describes a single syz-manager instance. @@ -114,6 +118,8 @@ type ReportingType interface { } type KernelRepo struct { + URL string + Branch string // Alias is a short, readable name of a kernel repository. Alias string // ReportingPriority says if we need to prefer to report crashes in this @@ -164,7 +170,14 @@ func installConfig(cfg *GlobalConfig) { if config != nil { panic("another config is already installed") } - // Validate the global cfg. + checkConfig(cfg) + config = cfg + initEmailReporting() + initHTTPHandlers() + initAPIHandlers() +} + +func checkConfig(cfg *GlobalConfig) { if len(cfg.Namespaces) == 0 { panic("no namespaces found") } @@ -178,23 +191,6 @@ func installConfig(cfg *GlobalConfig) { for ns, cfg := range cfg.Namespaces { checkNamespace(ns, cfg, namespaces, clientNames) } - for repo, info := range cfg.KernelRepos { - if info.Alias == "" { - panic(fmt.Sprintf("empty kernel repo alias for %q", repo)) - } - if prio := info.ReportingPriority; prio < 0 || prio > 9 { - panic(fmt.Sprintf("bad kernel repo reporting priority %v for %q", prio, repo)) - } - for _, email := range info.CC { - if _, err := mail.ParseAddress(email); err != nil { - panic(fmt.Sprintf("bad email address %q: %v", email, err)) - } - } - } - config = cfg - initEmailReporting() - initHTTPHandlers() - initAPIHandlers() } func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]bool) { @@ -231,6 +227,36 @@ func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]b return true } } + checkKernelRepos(ns, cfg) + checkNamespaceReporting(ns, cfg) +} + +func checkKernelRepos(ns string, cfg *Config) { + if len(cfg.Repos) == 0 { + panic(fmt.Sprintf("no repos in namespace %q", ns)) + } + for _, repo := range cfg.Repos { + if !vcs.CheckRepoAddress(repo.URL) { + panic(fmt.Sprintf("%v: bad repo URL %q", ns, repo.URL)) + } + if !vcs.CheckBranch(repo.Branch) { + panic(fmt.Sprintf("%v: bad repo branch %q", ns, repo.Branch)) + } + if repo.Alias == "" { + panic(fmt.Sprintf("%v: empty repo alias for %q", ns, repo.Alias)) + } + if prio := repo.ReportingPriority; prio < 0 || prio > 9 { + panic(fmt.Sprintf("%v: bad kernel repo reporting priority %v for %q", ns, prio, repo.Alias)) + } + for _, email := range repo.CC { + if _, err := mail.ParseAddress(email); err != nil { + panic(fmt.Sprintf("bad email address %q: %v", email, err)) + } + } + } +} + +func checkNamespaceReporting(ns string, cfg *Config) { checkConfigAccessLevel(&cfg.AccessLevel, cfg.AccessLevel, fmt.Sprintf("namespace %q", ns)) parentAccessLevel := cfg.AccessLevel reportingNames := make(map[string]bool) diff --git a/dashboard/app/email_test.go b/dashboard/app/email_test.go index 560dc54ca..557c4cccf 100644 --- a/dashboard/app/email_test.go +++ b/dashboard/app/email_test.go @@ -118,6 +118,8 @@ For more options, visit https://groups.google.com/d/optout. // Now report syz reproducer and check updated email. build2 := testBuild(10) + build2.KernelRepo = testConfig.Namespaces["test2"].Repos[0].URL + build2.KernelBranch = testConfig.Namespaces["test2"].Repos[0].Branch build2.KernelCommitTitle = "a really long title, longer than 80 chars, really long-long-long-long-long-long title" c.client2.UploadBuild(build2) crash.BuildID = build2.ID diff --git a/dashboard/app/entities.go b/dashboard/app/entities.go index 3ffe09b7d..705f4a26d 100644 --- a/dashboard/app/entities.go +++ b/dashboard/app/entities.go @@ -74,6 +74,7 @@ type Bug struct { NumRepro int64 ReproLevel dashapi.ReproLevel HasReport bool + NeedCommitInfo bool FirstTime time.Time LastTime time.Time LastSavedCrash time.Time @@ -82,9 +83,16 @@ type Bug struct { LastActivity time.Time // last time we observed any activity related to the bug Closed time.Time Reporting []BugReporting - Commits []string - HappenedOn []string `datastore:",noindex"` // list of managers - PatchedOn []string `datastore:",noindex"` // list of managers + Commits []string // titles of fixing commmits + CommitInfo []CommitInfo // git hashes of fixing commits (parallel array to Commits) + HappenedOn []string `datastore:",noindex"` // list of managers + PatchedOn []string `datastore:",noindex"` // list of managers +} + +type CommitInfo struct { + Hash string + Author string + Date time.Time } type BugReporting struct { @@ -362,18 +370,35 @@ func bugReportingHash(bugHash, reporting string) string { return hash.String([]byte(fmt.Sprintf("%v-%v", bugHash, reporting)))[:hashLen] } +func (bug *Bug) updateCommits(commits []string, now time.Time) { + bug.Commits = commits + bug.CommitInfo = nil + bug.NeedCommitInfo = true + bug.FixTime = now + bug.PatchedOn = nil +} + +func (bug *Bug) getCommitInfo(i int) CommitInfo { + if i < len(bug.CommitInfo) { + return bug.CommitInfo[i] + } + return CommitInfo{} +} + func kernelRepoInfo(build *Build) KernelRepo { - return kernelRepoInfoRaw(build.KernelRepo, build.KernelBranch) + return kernelRepoInfoRaw(build.Namespace, build.KernelRepo, build.KernelBranch) } -func kernelRepoInfoRaw(repo, branch string) KernelRepo { - repoID := repo - if branch != "" { - repoID += "/" + branch +func kernelRepoInfoRaw(ns, url, branch string) KernelRepo { + var info KernelRepo + for _, repo := range config.Namespaces[ns].Repos { + if repo.URL == url && repo.Branch == branch { + info = repo + break + } } - info := config.KernelRepos[repoID] if info.Alias == "" { - info.Alias = repo + info.Alias = url if branch != "" { info.Alias += " " + branch } diff --git a/dashboard/app/fix_test.go b/dashboard/app/fix_test.go index b0c7bee2b..448ee73a7 100644 --- a/dashboard/app/fix_test.go +++ b/dashboard/app/fix_test.go @@ -418,7 +418,10 @@ func TestFixedWithCommitTags(t *testing.T) { rep := c.client.pollBug() // Upload build with 2 fixing commits for this bug. - build1.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep.ID}, {"fix commit 2", rep.ID}} + build1.FixCommits = []dashapi.Commit{ + {Title: "fix commit 1", BugIDs: []string{rep.ID}}, + {Title: "fix commit 2", BugIDs: []string{rep.ID}}, + } c.client.UploadBuild(build1) // Now the commits must be associated with the bug and the second @@ -472,7 +475,9 @@ func TestFixedDup(t *testing.T) { c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID) // Upload build that fixes rep2. - build.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep2.ID}} + build.FixCommits = []dashapi.Commit{ + {Title: "fix commit 1", BugIDs: []string{rep2.ID}}, + } c.client.UploadBuild(build) // This must fix rep1. @@ -505,16 +510,11 @@ func TestFixedDup2(t *testing.T) { c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID) // Upload build that fixes rep2. - build1.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep2.ID}} + build1.FixCommits = []dashapi.Commit{ + {Title: "fix commit 1", BugIDs: []string{rep2.ID}}, + } c.client.UploadBuild(build1) - /* - dbBug1, _, _ := c.loadBug(rep1.ID) - t.Logf("BUG1: status=%v, commits: %+v, patched: %+v", dbBug1.Status, dbBug1.Commits, dbBug1.PatchedOn) - dbBug2, _, _ := c.loadBug(rep2.ID) - t.Logf("BUG2: status=%v, commits: %+v, patched: %+v", dbBug2.Status, dbBug2.Commits, dbBug2.PatchedOn) - */ - // Now undup the bugs. They are still unfixed as only 1 manager uploaded the commit. c.client.updateBug(rep2.ID, dashapi.BugStatusOpen, "") @@ -557,7 +557,10 @@ func TestFixedDup3(t *testing.T) { // Upload builds that fix rep1 and rep2 with different commits. // This must fix rep1 eventually and we must not livelock in such scenario. - build1.FixCommits = []dashapi.FixCommit{{"fix commit 1", rep1.ID}, {"fix commit 2", rep2.ID}} + build1.FixCommits = []dashapi.Commit{ + {Title: "fix commit 1", BugIDs: []string{rep1.ID}}, + {Title: "fix commit 2", BugIDs: []string{rep2.ID}}, + } build2.FixCommits = build1.FixCommits c.client.UploadBuild(build1) c.client.UploadBuild(build2) diff --git a/dashboard/app/index.yaml b/dashboard/app/index.yaml index 4adbf1527..56f82c937 100644 --- a/dashboard/app/index.yaml +++ b/dashboard/app/index.yaml @@ -22,6 +22,12 @@ indexes: - name: Seq direction: desc +- kind: Bug + properties: + - name: Namespace + - name: NeedCommitInfo + - name: Commits + - kind: Build properties: - name: Namespace diff --git a/dashboard/app/main.go b/dashboard/app/main.go index 044b4f767..8c3f624c8 100644 --- a/dashboard/app/main.go +++ b/dashboard/app/main.go @@ -15,6 +15,7 @@ import ( "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/email" "github.com/google/syzkaller/pkg/html" + "github.com/google/syzkaller/pkg/vcs" "golang.org/x/net/context" "google.golang.org/appengine/datastore" "google.golang.org/appengine/log" @@ -62,12 +63,21 @@ type uiManager struct { } type uiBuild struct { - Time time.Time - SyzkallerCommit string - KernelAlias string - KernelCommit string - KernelCommitDate time.Time - KernelConfigLink string + Time time.Time + SyzkallerCommit string + SyzkallerCommitLink string + KernelAlias string + KernelCommit string + KernelCommitLink string + KernelCommitDate time.Time + KernelConfigLink string +} + +type uiCommit struct { + Hash string + Title string + Link string + Date time.Time } type uiBugPage struct { @@ -119,7 +129,7 @@ type uiBug struct { Link string ExternalLink string CreditEmail string - Commits string + Commits []uiCommit PatchedOn []string MissingOn []string NumManagers int @@ -165,6 +175,7 @@ func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error var managers []*uiManager var jobs []*uiJob accessLevel := accessLevel(c, r) + if r.FormValue("fixed") == "" { var err error managers, err = loadManagers(c, accessLevel) @@ -422,7 +433,7 @@ func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string, id := uiBug.ReportingIndex if bug.Status == BugStatusFixed { id = -1 - } else if uiBug.Commits != "" { + } else if len(uiBug.Commits) != 0 { id = -2 } groups[id] = append(groups[id], uiBug) @@ -622,9 +633,14 @@ func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers [] } updateBugBadness(c, uiBug) if len(bug.Commits) != 0 { - uiBug.Commits = bug.Commits[0] - if len(bug.Commits) > 1 { - uiBug.Commits = fmt.Sprintf("%q", bug.Commits) + for i, com := range bug.Commits { + cfg := config.Namespaces[bug.Namespace] + info := bug.getCommitInfo(i) + uiBug.Commits = append(uiBug.Commits, uiCommit{ + Hash: info.Hash, + Title: com, + Link: vcs.CommitLink(cfg.Repos[0].URL, info.Hash), + }) } for _, mgr := range managers { found := false @@ -700,12 +716,14 @@ func loadCrashesForBug(c context.Context, bug *Bug) ([]*uiCrash, []byte, error) func makeUIBuild(build *Build) *uiBuild { return &uiBuild{ - Time: build.Time, - SyzkallerCommit: build.SyzkallerCommit, - KernelAlias: kernelRepoInfo(build).Alias, - KernelCommit: build.KernelCommit, - KernelCommitDate: build.KernelCommitDate, - KernelConfigLink: textLink(textKernelConfig, build.KernelConfig), + Time: build.Time, + SyzkallerCommit: build.SyzkallerCommit, + SyzkallerCommitLink: vcs.LogLink(vcs.SyzkallerRepo, build.SyzkallerCommit), + KernelAlias: kernelRepoInfo(build).Alias, + KernelCommit: build.KernelCommit, + KernelCommitLink: vcs.LogLink(build.KernelRepo, build.KernelCommit), + KernelCommitDate: build.KernelCommitDate, + KernelConfigLink: textLink(textKernelConfig, build.KernelConfig), } } @@ -813,7 +831,7 @@ func loadRecentJobs(c context.Context) ([]*uiJob, error) { Namespace: job.Namespace, Manager: job.Manager, BugTitle: job.BugTitle, - KernelAlias: kernelRepoInfoRaw(job.KernelRepo, job.KernelBranch).Alias, + KernelAlias: kernelRepoInfoRaw(job.Namespace, job.KernelRepo, job.KernelBranch).Alias, PatchLink: textLink(textPatch, job.Patch), Attempts: job.Attempts, Started: job.Started, diff --git a/dashboard/app/noaetest.go b/dashboard/app/noaetest.go new file mode 100644 index 000000000..5c461b779 --- /dev/null +++ b/dashboard/app/noaetest.go @@ -0,0 +1,8 @@ +// Copyright 2019 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. + +// +build !aetest + +package dash + +const isAppEngineTest = false diff --git a/dashboard/app/reporting.go b/dashboard/app/reporting.go index 32d3fc8e6..c038c67cb 100644 --- a/dashboard/app/reporting.go +++ b/dashboard/app/reporting.go @@ -511,9 +511,7 @@ func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, if len(cmd.FixCommits) != 0 && (bug.Status == BugStatusOpen || bug.Status == BugStatusDup) { sort.Strings(cmd.FixCommits) if !reflect.DeepEqual(bug.Commits, cmd.FixCommits) { - bug.Commits = cmd.FixCommits - bug.FixTime = now - bug.PatchedOn = nil + bug.updateCommits(cmd.FixCommits, now) } } if cmd.CrashID != 0 { diff --git a/dashboard/app/static/style.css b/dashboard/app/static/style.css index 6ac90a5f9..3a42bf93e 100644 --- a/dashboard/app/static/style.css +++ b/dashboard/app/static/style.css @@ -77,6 +77,11 @@ table td, table th { max-width: 350pt; } +.list_table .commit_list { + width: 500pt; + max-width: 500pt; +} + .list_table .tag { font-family: monospace; font-size: 8pt; @@ -101,8 +106,8 @@ table td, table th { } .list_table .kernel { - width: 60pt; - max-width: 60pt; + width: 80pt; + max-width: 80pt; } .list_table .maintainers { @@ -151,3 +156,7 @@ textarea { width:100%; font-family: monospace; } + +.mono { + font-family: monospace; +} diff --git a/dashboard/app/templates.html b/dashboard/app/templates.html index 2047db570..4c81af27e 100644 --- a/dashboard/app/templates.html +++ b/dashboard/app/templates.html @@ -82,10 +82,10 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the </td> {{if $.ShowPatch}} <td class="stat">{{formatLateness $.Now $b.ClosedTime}}</td> - <td class="title" title="{{$b.Commits}}">{{$b.Commits}}</td> + <td class="commit_list">{{template "fix_commits" $b.Commits}}</td> {{end}} {{if $.ShowPatched}} - <td class="patched" title="{{$b.Commits}}">{{if $b.Commits}}{{len $b.PatchedOn}}/{{$b.NumManagers}}{{end}}</td> + <td class="patched" {{if $b.Commits}}title="{{with $com := index $b.Commits 0}}{{$com.Title}}{{end}}"{{end}}>{{len $b.PatchedOn}}/{{$b.NumManagers}}</td> {{end}} {{if $.ShowStatus}} <td class="status"> @@ -130,11 +130,11 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the </td> <td class="stat {{if $mgr.LastActiveBad}}bad{{end}}">{{formatLateness $mgr.Now $mgr.LastActive}}</td> <td class="stat">{{formatDuration $mgr.CurrentUpTime}}</td> - {{if $mgr.CurrentBuild}} - <td class="stat">{{formatLateness $mgr.Now $mgr.CurrentBuild.Time}}</td> - <td class="stat" title="{{$mgr.CurrentBuild.KernelAlias}} {{$mgr.CurrentBuild.KernelCommit}} -{{formatTime $mgr.CurrentBuild.KernelCommitDate}}">{{formatShortHash $mgr.CurrentBuild.KernelCommit}}</td> - <td class="stat" title="{{$mgr.CurrentBuild.SyzkallerCommit}}">{{formatShortHash $mgr.CurrentBuild.SyzkallerCommit}}</td> + {{with $build := $mgr.CurrentBuild}} + <td class="stat">{{formatLateness $mgr.Now $build.Time}}</td> + <td class="stat" title="{{$build.KernelAlias}} {{$build.KernelCommit}} +{{formatTime $build.KernelCommitDate}}">{{link $build.KernelCommitLink (formatShortHash $mgr.CurrentBuild.KernelCommit)}}</td> + <td class="stat">{{link $mgr.CurrentBuild.SyzkallerCommitLink (formatShortHash $mgr.CurrentBuild.SyzkallerCommit)}}</td> {{else}} <td></td> <td></td> @@ -158,3 +158,15 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the </table> {{end}} {{end}} + +{{/* List of fixing commits, invoked with []*uiCommit */}} +{{define "fix_commits"}} +{{range $com := .}} + <span class="mono"> + {{if $com.Hash}} + {{formatShortHash $com.Hash}} + {{end}} + {{link $com.Link $com.Title}} + </span> +{{end}} +{{end}} diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go index bec406d78..02d7ea132 100644 --- a/dashboard/dashapi/dashapi.go +++ b/dashboard/dashapi/dashapi.go @@ -68,12 +68,15 @@ type Build struct { KernelCommitDate time.Time KernelConfig []byte Commits []string // see BuilderPoll - FixCommits []FixCommit + FixCommits []Commit } -type FixCommit struct { - Title string - BugID string +type Commit struct { + Hash string + Title string + Author string + BugIDs []string // ID's extracted from Reported-by tags + Date time.Time } func (dash *Dashboard) UploadBuild(build *Build) error { @@ -161,6 +164,34 @@ func (dash *Dashboard) ReportBuildError(req *BuildErrorReq) error { return dash.Query("report_build_error", req, nil) } +type CommitPollResp struct { + ReportEmail string + Repos []Repo + Commits []string +} + +type CommitPollResultReq struct { + Commits []Commit +} + +type Repo struct { + URL string + Branch string +} + +func (dash *Dashboard) CommitPoll() (*CommitPollResp, error) { + resp := new(CommitPollResp) + err := dash.Query("commit_poll", nil, resp) + return resp, err +} + +func (dash *Dashboard) UploadCommits(commits []Commit) error { + if len(commits) == 0 { + return nil + } + return dash.Query("upload_commits", &CommitPollResultReq{commits}, nil) +} + // Crash describes a single kernel crash (potentially with repro). type Crash struct { BuildID string // refers to Build.ID |
