Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1113)

Side by Side Diff: milo/appengine/job_source/buildbot/build.go

Issue 2949783002: [milo] appengine/* -> * (Closed)
Patch Set: rebase Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 package buildbot
6
7 import (
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io/ioutil"
12 "math"
13 "path/filepath"
14 "regexp"
15 "sort"
16 "strconv"
17 "strings"
18 "time"
19
20 "golang.org/x/net/context"
21
22 "github.com/luci/gae/service/datastore"
23 "github.com/luci/luci-go/common/data/stringset"
24 "github.com/luci/luci-go/common/logging"
25 "github.com/luci/luci-go/milo/api/resp"
26 "github.com/luci/luci-go/milo/appengine/common/model"
27 )
28
29 var errBuildNotFound = errors.New("Build not found")
30
31 // getBuild fetches a buildbot build from the datastore and checks ACLs.
32 // The return code matches the master responses.
33 func getBuild(c context.Context, master, builder string, buildNum int) (*buildbo tBuild, error) {
34 result := &buildbotBuild{
35 Master: master,
36 Buildername: builder,
37 Number: buildNum,
38 }
39
40 err := datastore.Get(c, result)
41 err = checkAccess(c, err, result.Internal)
42 if err == errMasterNotFound {
43 err = errBuildNotFound
44 }
45
46 return result, err
47 }
48
49 // result2Status translates a buildbot result integer into a model.Status.
50 func result2Status(s *int) (status model.Status) {
51 if s == nil {
52 return model.Running
53 }
54 switch *s {
55 case 0:
56 status = model.Success
57 case 1:
58 status = model.Warning
59 case 2:
60 status = model.Failure
61 case 3:
62 status = model.NotRun // Skipped
63 case 4:
64 status = model.Exception
65 case 5:
66 status = model.WaitingDependency // Retry
67 default:
68 panic(fmt.Errorf("Unknown status %d", s))
69 }
70 return
71 }
72
73 // buildbotTimeToTime converts a buildbot time representation (pointer to float
74 // of seconds since epoch) to a native time.Time object.
75 func buildbotTimeToTime(t *float64) (result time.Time) {
76 if t != nil {
77 result = time.Unix(int64(*t), int64(*t*1e9)%1e9).UTC()
78 }
79 return
80 }
81
82 // parseTimes translates a buildbot time tuple (start, end) into a triplet
83 // of (Started time, Ending time, duration).
84 // If times[1] is nil and buildFinished is not, ended will be set to buildFinish ed
85 // time.
86 func parseTimes(buildFinished *float64, times []*float64) (started, ended time.T ime, duration time.Duration) {
87 if len(times) != 2 {
88 panic(fmt.Errorf("Expected 2 floats for times, got %v", times))
89 }
90 if times[0] == nil {
91 // Some steps don't have timing info. In that case, just return nils.
92 return
93 }
94 started = buildbotTimeToTime(times[0])
95 switch {
96 case times[1] != nil:
97 ended = buildbotTimeToTime(times[1])
98 duration = ended.Sub(started)
99 case buildFinished != nil:
100 ended = buildbotTimeToTime(buildFinished)
101 duration = ended.Sub(started)
102 default:
103 duration = time.Since(started)
104 }
105 return
106 }
107
108 // getBanner parses the OS information from the build and maybe returns a banner .
109 func getBanner(c context.Context, b *buildbotBuild) *resp.LogoBanner {
110 logging.Infof(c, "OS: %s/%s", b.OSFamily, b.OSVersion)
111 osLogo := func() *resp.Logo {
112 result := &resp.Logo{}
113 switch b.OSFamily {
114 case "windows":
115 result.LogoBase = resp.Windows
116 case "Darwin":
117 result.LogoBase = resp.OSX
118 case "Debian":
119 result.LogoBase = resp.Ubuntu
120 default:
121 return nil
122 }
123 result.Subtitle = b.OSVersion
124 return result
125 }()
126 if osLogo != nil {
127 return &resp.LogoBanner{
128 OS: []resp.Logo{*osLogo},
129 }
130 }
131 logging.Warningf(c, "No OS info found.")
132 return nil
133 }
134
135 // summary extracts the top level summary from a buildbot build as a
136 // BuildComponent
137 func summary(c context.Context, b *buildbotBuild) resp.BuildComponent {
138 // TODO(hinoka): use b.toStatus()
139 // Status
140 var status model.Status
141 if b.Currentstep != nil {
142 status = model.Running
143 } else {
144 status = result2Status(b.Results)
145 }
146
147 // Timing info
148 started, ended, duration := parseTimes(nil, b.Times)
149
150 // Link to bot and original build.
151 host := "build.chromium.org/p"
152 if b.Internal {
153 host = "uberchromegw.corp.google.com/i"
154 }
155 bot := resp.NewLink(
156 b.Slave,
157 fmt.Sprintf("https://%s/%s/buildslaves/%s", host, b.Master, b.Sl ave),
158 )
159 source := resp.NewLink(
160 fmt.Sprintf("%s/%s/%d", b.Master, b.Buildername, b.Number),
161 fmt.Sprintf("https://%s/%s/builders/%s/builds/%d",
162 host, b.Master, b.Buildername, b.Number),
163 )
164
165 // The link to the builder page.
166 parent := resp.NewLink(b.Buildername, ".")
167
168 // Do a best effort lookup for the bot information to fill in OS/Platfor m info.
169 banner := getBanner(c, b)
170
171 sum := resp.BuildComponent{
172 ParentLabel: parent,
173 Label: fmt.Sprintf("#%d", b.Number),
174 Banner: banner,
175 Status: status,
176 Started: started,
177 Finished: ended,
178 Bot: bot,
179 Source: source,
180 Duration: duration,
181 Type: resp.Summary, // This is more or less ignored.
182 LevelsDeep: 1,
183 Text: []string{}, // Status messages. Eg "This build fai led on..xyz"
184 }
185
186 return sum
187 }
188
189 var rLineBreak = regexp.MustCompile("<br */?>")
190
191 // components takes a full buildbot build struct and extract step info from all
192 // of the steps and returns it as a list of milo Build Components.
193 func components(b *buildbotBuild) (result []*resp.BuildComponent) {
194 endingTime := b.Times[1]
195 for _, step := range b.Steps {
196 if step.Hidden == true {
197 continue
198 }
199 bc := &resp.BuildComponent{
200 Label: step.Name,
201 }
202 // Step text sometimes contains <br>, which we want to parse int o new lines.
203 for _, t := range step.Text {
204 for _, line := range rLineBreak.Split(t, -1) {
205 bc.Text = append(bc.Text, line)
206 }
207 }
208
209 // Figure out the status.
210 if !step.IsStarted {
211 bc.Status = model.NotRun
212 } else if !step.IsFinished {
213 bc.Status = model.Running
214 } else {
215 if len(step.Results) > 0 {
216 status := int(step.Results[0].(float64))
217 bc.Status = result2Status(&status)
218 } else {
219 bc.Status = model.Success
220 }
221 }
222
223 // Raise the interesting-ness if the step is not "Success".
224 if bc.Status != model.Success {
225 bc.Verbosity = resp.Interesting
226 }
227
228 remainingAliases := stringset.New(len(step.Aliases))
229 for linkAnchor := range step.Aliases {
230 remainingAliases.Add(linkAnchor)
231 }
232
233 getLinksWithAliases := func(logLink *resp.Link, isLog bool) resp .LinkSet {
234 // Generate alias links.
235 var aliases resp.LinkSet
236 if remainingAliases.Del(logLink.Label) {
237 stepAliases := step.Aliases[logLink.Label]
238 aliases = make(resp.LinkSet, len(stepAliases))
239 for i, alias := range stepAliases {
240 aliases[i] = alias.toLink()
241 }
242 }
243
244 // Step log link takes primary, with aliases as secondar y.
245 links := make(resp.LinkSet, 1, 1+len(aliases))
246 links[0] = logLink
247
248 for _, a := range aliases {
249 a.Alias = true
250 }
251 return append(links, aliases...)
252 }
253
254 for _, l := range step.Logs {
255 logLink := resp.NewLink(l[0], l[1])
256
257 links := getLinksWithAliases(logLink, true)
258 if logLink.Label == "stdio" {
259 bc.MainLink = links
260 } else {
261 bc.SubLink = append(bc.SubLink, links)
262 }
263 }
264
265 // Step links are stored as maps of name: url
266 // Because Go doesn't believe in nice things, we now create anot her array
267 // just so that we can iterate through this map in order.
268 names := make([]string, 0, len(step.Urls))
269 for name := range step.Urls {
270 names = append(names, name)
271 }
272 sort.Strings(names)
273 for _, name := range names {
274 logLink := resp.NewLink(name, step.Urls[name])
275
276 bc.SubLink = append(bc.SubLink, getLinksWithAliases(logL ink, false))
277 }
278
279 // Add any unused aliases directly.
280 if remainingAliases.Len() > 0 {
281 unusedAliases := remainingAliases.ToSlice()
282 sort.Strings(unusedAliases)
283
284 for _, label := range unusedAliases {
285 var baseLink resp.LinkSet
286 for _, alias := range step.Aliases[label] {
287 aliasLink := alias.toLink()
288 if len(baseLink) == 0 {
289 aliasLink.Label = label
290 } else {
291 aliasLink.Alias = true
292 }
293 baseLink = append(baseLink, aliasLink)
294 }
295
296 if len(baseLink) > 0 {
297 bc.SubLink = append(bc.SubLink, baseLink )
298 }
299 }
300 }
301
302 // Figure out the times.
303 bc.Started, bc.Finished, bc.Duration = parseTimes(endingTime, st ep.Times)
304
305 result = append(result, bc)
306 }
307 return
308 }
309
310 // parseProp returns a string representation of v.
311 func parseProp(v interface{}) string {
312 // if v is a whole number, force it into an int. json.Marshal() would t urn
313 // it into what looks like a float instead. We want this to remain and
314 // int instead of a number.
315 if vf, ok := v.(float64); ok {
316 if math.Floor(vf) == vf {
317 return fmt.Sprintf("%d", int64(vf))
318 }
319 }
320 // return the json representation of the value.
321 b, err := json.Marshal(v)
322 if err == nil {
323 return string(b)
324 }
325 return fmt.Sprintf("%v", v)
326 }
327
328 // Prop is a struct used to store a value and group so that we can make a map
329 // of key:Prop to pass into parseProp() for the purpose of cross referencing
330 // one prop while working on another.
331 type Prop struct {
332 Value interface{}
333 Group string
334 }
335
336 // properties extracts all properties from buildbot builds and groups them into
337 // property groups.
338 func properties(b *buildbotBuild) (result []*resp.PropertyGroup) {
339 groups := map[string]*resp.PropertyGroup{}
340 allProps := map[string]Prop{}
341 for _, prop := range b.Properties {
342 allProps[prop.Name] = Prop{
343 Value: prop.Value,
344 Group: prop.Source,
345 }
346 }
347 for key, prop := range allProps {
348 value := prop.Value
349 groupName := prop.Group
350 if _, ok := groups[groupName]; !ok {
351 groups[groupName] = &resp.PropertyGroup{GroupName: group Name}
352 }
353 vs := parseProp(value)
354 groups[groupName].Property = append(groups[groupName].Property, &resp.Property{
355 Key: key,
356 Value: vs,
357 })
358 }
359 // Insert the groups into a list in alphabetical order.
360 // You have to make a separate sorting data structure because Go doesn't like
361 // sorting things for you.
362 groupNames := []string{}
363 for n := range groups {
364 groupNames = append(groupNames, n)
365 }
366 sort.Strings(groupNames)
367 for _, k := range groupNames {
368 group := groups[k]
369 // Also take this oppertunity to sort the properties within the groups.
370 sort.Sort(group)
371 result = append(result, group)
372 }
373 return
374 }
375
376 // blame extracts the commit and blame information from a buildbot build and
377 // returns it as a list of Commits.
378 func blame(b *buildbotBuild) (result []*resp.Commit) {
379 for _, c := range b.Sourcestamp.Changes {
380 files := c.GetFiles()
381 result = append(result, &resp.Commit{
382 AuthorEmail: c.Who,
383 Repo: c.Repository,
384 CommitTime: time.Unix(int64(c.When), 0).UTC(),
385 Revision: resp.NewLink(c.Revision, c.Revlink),
386 Description: c.Comments,
387 Title: strings.Split(c.Comments, "\n")[0],
388 File: files,
389 })
390 }
391 return
392 }
393
394 // sourcestamp extracts the source stamp from various parts of a buildbot build,
395 // including the properties.
396 func sourcestamp(c context.Context, b *buildbotBuild) *resp.SourceStamp {
397 ss := &resp.SourceStamp{}
398 rietveld := ""
399 gerrit := ""
400 got_revision := ""
401 repository := ""
402 issue := int64(-1)
403 for _, prop := range b.Properties {
404 switch prop.Name {
405 case "rietveld":
406 if v, ok := prop.Value.(string); ok {
407 rietveld = v
408 } else {
409 logging.Warningf(c, "Field rietveld is not a str ing: %#v", prop.Value)
410 }
411 case "issue":
412 // Sometime this is a number (float), sometime it is a s tring.
413 if v, ok := prop.Value.(float64); ok {
414 issue = int64(v)
415 } else if v, ok := prop.Value.(string); ok {
416 if vi, err := strconv.ParseInt(v, 10, 64); err = = nil {
417 issue = int64(vi)
418 } else {
419 logging.Warningf(c, "Could not decode fi eld issue: %q - %s", prop.Value, err)
420 }
421 } else {
422 logging.Warningf(c, "Field issue is not a string or float: %#v", prop.Value)
423 }
424
425 case "got_revision":
426 if v, ok := prop.Value.(string); ok {
427 got_revision = v
428 } else {
429 logging.Warningf(c, "Field got_revision is not a string: %#v", prop.Value)
430 }
431
432 case "patch_issue":
433 if v, ok := prop.Value.(float64); ok {
434 issue = int64(v)
435 } else {
436 logging.Warningf(c, "Field patch_issue is not a float: %#v", prop.Value)
437 }
438
439 case "patch_gerrit_url":
440 if v, ok := prop.Value.(string); ok {
441 gerrit = v
442 } else {
443 logging.Warningf(c, "Field gerrit is not a strin g: %#v", prop.Value)
444 }
445
446 case "repository":
447 if v, ok := prop.Value.(string); ok {
448 repository = v
449 }
450 }
451 }
452 if issue != -1 {
453 switch {
454 case rietveld != "":
455 rietveld = strings.TrimRight(rietveld, "/")
456 ss.Changelist = resp.NewLink(
457 fmt.Sprintf("Rietveld CL %d", issue),
458 fmt.Sprintf("%s/%d", rietveld, issue))
459 case gerrit != "":
460 gerrit = strings.TrimRight(gerrit, "/")
461 ss.Changelist = resp.NewLink(
462 fmt.Sprintf("Gerrit CL %d", issue),
463 fmt.Sprintf("%s/c/%d", gerrit, issue))
464 }
465 }
466
467 if got_revision != "" {
468 ss.Revision = resp.NewLink(got_revision, "")
469 if repository != "" {
470 ss.Revision.URL = repository + "/+/" + got_revision
471 }
472 }
473 return ss
474 }
475
476 func renderBuild(c context.Context, b *buildbotBuild) *resp.MiloBuild {
477 // Modify the build for rendering.
478 updatePostProcessBuild(b)
479
480 // TODO(hinoka): Do all fields concurrently.
481 return &resp.MiloBuild{
482 SourceStamp: sourcestamp(c, b),
483 Summary: summary(c, b),
484 Components: components(b),
485 PropertyGroup: properties(b),
486 Blame: blame(b),
487 }
488 }
489
490 // DebugBuild fetches a debugging build for testing.
491 func DebugBuild(c context.Context, relBuildbotDir string, builder string, buildN um int) (*resp.MiloBuild, error) {
492 fname := fmt.Sprintf("%s.%d.json", builder, buildNum)
493 // ../buildbot below assumes that
494 // - this code is not executed by tests outside of this dir
495 // - this dir is a sibling of frontend dir
496 path := filepath.Join(relBuildbotDir, "testdata", fname)
497 raw, err := ioutil.ReadFile(path)
498 if err != nil {
499 return nil, err
500 }
501 b := &buildbotBuild{}
502 if err := json.Unmarshal(raw, b); err != nil {
503 return nil, err
504 }
505 return renderBuild(c, b), nil
506 }
507
508 // Build fetches a buildbot build and translates it into a miloBuild.
509 func Build(c context.Context, master, builder string, buildNum int) (*resp.MiloB uild, error) {
510 b, err := getBuild(c, master, builder, buildNum)
511 if err != nil {
512 return nil, err
513 }
514 return renderBuild(c, b), nil
515 }
516
517 // updatePostProcessBuild transforms a build from its raw JSON format into the
518 // format that should be presented to users.
519 //
520 // Post-processing includes:
521 // - If the build is LogDog-only, promotes aliases (LogDog links) to
522 // first-class links in the build.
523 func updatePostProcessBuild(b *buildbotBuild) {
524 // If this is a LogDog-only build, we want to promote the LogDog links.
525 if loc, ok := b.getPropertyValue("log_location").(string); ok && strings .HasPrefix(loc, "logdog://") {
526 linkMap := map[string]string{}
527 for sidx := range b.Steps {
528 promoteLogDogLinks(&b.Steps[sidx], sidx == 0, linkMap)
529 }
530
531 // Update "Logs". This field is part of BuildBot, and is the ama lgamation
532 // of all logs in the build's steps. Since each log is out of co ntext of its
533 // original step, we can't apply the promotion logic; instead, w e will use
534 // the link map to map any old URLs that were matched in "promot eLogDogLnks"
535 // to their new URLs.
536 for _, link := range b.Logs {
537 // "link" is in the form: [NAME, URL]
538 if len(link) != 2 {
539 continue
540 }
541
542 if newURL, ok := linkMap[link[1]]; ok {
543 link[1] = newURL
544 }
545 }
546 }
547 }
548
549 // promoteLogDogLinks updates the links in a BuildBot step to
550 // promote LogDog links.
551 //
552 // A build's links come in one of three forms:
553 // - Log Links, which link directly to BuildBot build logs.
554 // - URL Links, which are named links to arbitrary URLs.
555 // - Aliases, which attach to the label in one of the other types of links and
556 // augment it with additional named links.
557 //
558 // LogDog uses aliases exclusively to attach LogDog logs to other links. When
559 // the build is LogDog-only, though, the original links are actually junk. What
560 // we want to do is remove the original junk links and replace them with their
561 // alias counterparts, so that the "natural" BuildBot links are actually LogDog
562 // links.
563 //
564 // As URLs are re-mapped, the supplied "linkMap" will be updated to map the old
565 // URLs to the new ones.
566 func promoteLogDogLinks(s *buildbotStep, isInitialStep bool, linkMap map[string] string) {
567 type stepLog struct {
568 label string
569 url string
570 }
571
572 remainingAliases := stringset.New(len(s.Aliases))
573 for linkAnchor := range s.Aliases {
574 remainingAliases.Add(linkAnchor)
575 }
576
577 maybePromoteAliases := func(sl *stepLog, isLog bool) []*stepLog {
578 // As a special case, if this is the first step ("steps" in Buil dBot), we
579 // will refrain from promoting aliases for "stdio", since "stdio " represents
580 // the raw BuildBot logs.
581 if isLog && isInitialStep && sl.label == "stdio" {
582 // No aliases, don't modify this log.
583 return []*stepLog{sl}
584 }
585
586 // If there are no aliases, we should obviously not promote them . This will
587 // be the case for pre-LogDog steps such as build setup.
588 aliases := s.Aliases[sl.label]
589 if len(aliases) == 0 {
590 return []*stepLog{sl}
591 }
592
593 // We have chosen to promote the aliases. Therefore, we will not include
594 // them as aliases in the modified step.
595 remainingAliases.Del(sl.label)
596
597 result := make([]*stepLog, len(aliases))
598 for i, alias := range aliases {
599 aliasStepLog := stepLog{alias.Text, alias.URL}
600
601 // Any link named "logdog" (Annotee cosmetic implementat ion detail) will
602 // inherit the name of the original log.
603 if isLog {
604 if aliasStepLog.label == "logdog" {
605 aliasStepLog.label = sl.label
606 }
607 }
608
609 result[i] = &aliasStepLog
610 }
611
612 // If we performed mapping, add the OLD -> NEW URL mapping to li nkMap.
613 //
614 // Since multpiple aliases can apply to a single log, and we hav e to pick
615 // one, here, we'll arbitrarily pick the last one. This is maybe more
616 // consistent than the first one because linkMap, itself, will e nd up
617 // holding the last mapping for any given URL.
618 if len(result) > 0 {
619 linkMap[sl.url] = result[len(result)-1].url
620 }
621
622 return result
623 }
624
625 // Update step logs.
626 newLogs := make([][]string, 0, len(s.Logs))
627 for _, l := range s.Logs {
628 for _, res := range maybePromoteAliases(&stepLog{l[0], l[1]}, tr ue) {
629 newLogs = append(newLogs, []string{res.label, res.url})
630 }
631 }
632 s.Logs = newLogs
633
634 // Update step URLs.
635 newURLs := make(map[string]string, len(s.Urls))
636 for label, link := range s.Urls {
637 urlLinks := maybePromoteAliases(&stepLog{label, link}, false)
638 if len(urlLinks) > 0 {
639 // Use the last URL link, since our URL map can only tol erate one link.
640 // The expected case here is that len(urlLinks) == 1, th ough, but it's
641 // possible that multiple aliases can be included for a single URL, so
642 // we need to handle that.
643 newValue := urlLinks[len(urlLinks)-1]
644 newURLs[newValue.label] = newValue.url
645 } else {
646 newURLs[label] = link
647 }
648 }
649 s.Urls = newURLs
650
651 // Preserve any aliases that haven't been promoted.
652 var newAliases map[string][]*buildbotLinkAlias
653 if l := remainingAliases.Len(); l > 0 {
654 newAliases = make(map[string][]*buildbotLinkAlias, l)
655 remainingAliases.Iter(func(v string) bool {
656 newAliases[v] = s.Aliases[v]
657 return true
658 })
659 }
660 s.Aliases = newAliases
661 }
OLDNEW
« no previous file with comments | « milo/appengine/frontend/templates/pages/settings.html ('k') | milo/appengine/job_source/buildbot/build_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698