mirror of
git://git.yoctoproject.org/yocto-autobuilder2.git
synced 2025-07-04 20:54:48 +02:00

Initial prototype of using yocto-autobuilder-helper scripts from vanilla buildbot to replicate yocto-autobuilder configuration. * README.md is updated to describe goals and approach * TODO contains known issues and work items, TODO: comments in the code point to specific locations of work Signed-off-by: Joshua Lock <joshua.g.lock@intel.com>
408 lines
16 KiB
Python
408 lines
16 KiB
Python
from buildbot.reporters import utils
|
|
from buildbot.util import service
|
|
from twisted.internet import defer
|
|
from twisted.python import log
|
|
|
|
from yoctoab.lib.wiki import YPWiki
|
|
|
|
|
|
class WikiLog(service.BuildbotService):
|
|
name = "WikiLog"
|
|
wiki = None
|
|
|
|
def checkConfig(self, wiki_uri, wiki_un, wiki_pass, wiki_page,
|
|
identifier=None, **kwargs):
|
|
service.BuildbotService.checkConfig(self)
|
|
|
|
@defer.inlineCallbacks
|
|
def reconfigService(self, wiki_uri, wiki_un, wiki_pass, wiki_page,
|
|
identifier=None, **kwargs):
|
|
yield service.BuildbotService.reconfigService(self)
|
|
self.wiki_page = wiki_page
|
|
tagfmt = " on {}"
|
|
if identifier:
|
|
self.tag = tagfmt(identifier)
|
|
else:
|
|
self.tag = ""
|
|
self.wiki = YPWiki(wiki_uri, wiki_un, wiki_pass)
|
|
|
|
@defer.inlineCallbacks
|
|
def startService(self):
|
|
yield service.BuildbotService.startService(self)
|
|
|
|
startConsuming = self.master.mq.startConsuming
|
|
self._buildCompleteConsumer = yield startConsuming(
|
|
self.buildFinished,
|
|
('builds', None, 'finished'))
|
|
|
|
self._buildStartedConsumer = yield startConsuming(
|
|
self.buildStarted,
|
|
('builds', None, 'new'))
|
|
|
|
# TODO: stepFinished? Or do we no longer need that now that the build
|
|
# is much simpler?
|
|
|
|
def stopService(self):
|
|
self._buildCompleteConsumer.stopConsuming()
|
|
self._buildStartedConsumer.stopConsuming()
|
|
|
|
def buildStarted(self, key, build):
|
|
yield utils.getDetailsForBuild(self.master, build,
|
|
**self.needed)
|
|
builderName = build['builder']['name']
|
|
# TODO: this all seems a bit overly complex?
|
|
if builderName != "nightly" and not self.isTriggered(build)\
|
|
and not self.isNightlyRunning():
|
|
# If a build has been started which isn't nightly and a nightly
|
|
# build isn't running, chances are a single builder has been
|
|
# started in order to test something and it just finished.
|
|
if not self.logBuild(build):
|
|
log.err("wkl: Failed to log build %s on %s" % (
|
|
build.getNumber(), builderName))
|
|
elif builderName == "nightly":
|
|
if not self.logBuild(build):
|
|
log.err("wkl: Failed to log build %s on %s" % (
|
|
build.getNumber(), builderName))
|
|
|
|
def stepFinished(self, key, build):
|
|
blurb, content = self.wiki.get_content(self.wiki_page)
|
|
if not blurb:
|
|
log.err("wkl: Couldn't get wiki content in stepFinished()")
|
|
return
|
|
update = self.updateBuildInfo(content, build)
|
|
|
|
if not update:
|
|
# log.msg("wkl: No update, nothing to POST")
|
|
return
|
|
|
|
if content == update:
|
|
# log.msg("wkl: No update made, no need to make a POST")
|
|
return
|
|
|
|
cookies = self.wiki.login()
|
|
if not cookies:
|
|
log.err("wkl: Failed to login to wiki")
|
|
return
|
|
|
|
summary = "Updating branch and commitish for %s" %\
|
|
build.getNumber()
|
|
if not self.wiki.post_entry(self.wiki_page, blurb+update, summary,
|
|
cookies):
|
|
# log.err("wkl: Failed to update wikilog with summary: '{}'".format
|
|
# (summary))
|
|
return
|
|
|
|
def buildFinished(self, key, build):
|
|
# TODO: we don't have a result var
|
|
if result == SUCCESS:
|
|
return
|
|
|
|
if not self.updateBuild(build):
|
|
log.err("wkl: Failed to update wikilog with build %s failure" %
|
|
build.getNumber())
|
|
|
|
def logBuild(self, build):
|
|
"""
|
|
Extract information about 'build' and post an entry to the wiki
|
|
|
|
@type build: buildbot.status.build.BuildStatus
|
|
"""
|
|
|
|
builder = build.getBuilder().getName()
|
|
reason = build.getReason()
|
|
buildid = str(build.getNumber())
|
|
start, _ = build.getTimes()
|
|
url = self.status.getURLForThing(build)
|
|
buildbranch = build.getProperty('branch').strip()
|
|
if not buildbranch or len(buildbranch) < 1:
|
|
buildbranch = "YP_BUILDBRANCH"
|
|
chash = build.getProperty('commit_poky').strip()
|
|
if not chash or len(chash) < 1 or chash == "HEAD":
|
|
chash = "YP_CHASH"
|
|
|
|
reason_list = reason.split(':', 1)
|
|
forcedby = reason_list[0].strip()
|
|
description = 'No reason given.'
|
|
if len(reason_list) > 1 and reason_list[1] != ' ':
|
|
description = reason_list[1].strip()
|
|
starttime = time.ctime(start)
|
|
|
|
sectionfmt = '==[{} {} {} - {} {}]=='
|
|
section_title = sectionfmt.format(url, builder, buildid, buildbranch,
|
|
chash)
|
|
summaryfmt = 'Adding new BuildLog entry for build %s (%s)'
|
|
summary = summaryfmt % (buildid, chash)
|
|
summary = summary + self.tag
|
|
content = "* '''Build ID''' - %s" % chash
|
|
content = content + self.tag + "\n"
|
|
content = content + '* Started at: %s\n' % starttime
|
|
content = content + '* ' + forcedby + '\n* ' + description + '\n'
|
|
new_entry = '{}\n{}\n'.format(section_title, content).encode('utf-8')
|
|
|
|
blurb, entries = self.wiki.get_content(self.wiki_page)
|
|
if not blurb:
|
|
log.err("wkl: Unexpected content retrieved from wiki!")
|
|
return False
|
|
|
|
entries = new_entry + entries
|
|
cookies = self.wiki.login()
|
|
|
|
if not cookies:
|
|
log.err("wkl: Failed to login to wiki")
|
|
return False
|
|
|
|
if not self.wiki.post_entry(self.wiki_page, blurb+entries,
|
|
summary, cookies):
|
|
log.err("wkl: Failed to post entry for %s" % buildid)
|
|
return False
|
|
|
|
log.msg("wkl: Posting wikilog entry for %s" % buildid)
|
|
return True
|
|
|
|
def updateEntryBuildInfo(self, entry, build):
|
|
"""
|
|
Extract the branch and commit hash from the properties of the 'build'
|
|
and update the 'entry' string with extracted values
|
|
|
|
@type entry: string
|
|
@type build: buildbot.status.build.BuildStatus
|
|
"""
|
|
# We only want to update the commit and branch info for the
|
|
# primary poky build
|
|
# FIXME: this is quite poky specific. Can we handle this in
|
|
# a more generic manner?
|
|
repo = build.getProperty("repourl_poky")
|
|
if not repo:
|
|
return entry
|
|
buildbranch = build.getProperty('branch').strip()
|
|
if not buildbranch or len(buildbranch) < 1:
|
|
buildbranch = "YP_BUILDBRANCH"
|
|
chash = build.getProperty('commit_poky').strip()
|
|
if not chash or len(chash) < 1 or chash == "HEAD":
|
|
chash = "YP_CHASH"
|
|
|
|
new_entry = entry.replace("YP_BUILDBRANCH", buildbranch, 1)
|
|
new_entry = new_entry.replace("YP_CHASH", chash, 2)
|
|
|
|
return new_entry
|
|
|
|
def updateBuildInfo(self, content, build):
|
|
"""
|
|
Extract the branch and commit hash from the properties of the 'build'
|
|
and update the 'content' string with extracted values
|
|
|
|
@type content: string
|
|
@type build: buildbot.status.build.BuildStatus
|
|
"""
|
|
|
|
# Try to find an entry that matches this build, rather than blindly
|
|
# updating all instances of the template value in the content
|
|
buildid = build.getProperty('buildnumber', '0')
|
|
builder = build.getProperty('buildername', 'nobuilder')
|
|
entry_list = re.split('\=\=\[(.+)\]\=\=', content)
|
|
title_idx = -1
|
|
# Start at the beginning of entry list and keep iterating until we find
|
|
# a title which looks ~right
|
|
for idx, ent in enumerate(entry_list):
|
|
# The matched title contents should always start with a http*
|
|
# schemed URI
|
|
if ent.startswith('http'):
|
|
# format of the title is:
|
|
# ==[url builder buildid - buildbranch commit_hash]==
|
|
title_components = ent.split(None, 6)
|
|
if builder == title_components[1] and \
|
|
str(buildid) == title_components[2]:
|
|
title_idx = idx
|
|
break
|
|
|
|
if title_idx < 0:
|
|
errmsg = ("wkl: Failed to update entry for {0} couldn't find a "
|
|
"matching title with builder {1}")
|
|
log.err(errmsg.format(buildid, builder))
|
|
return content
|
|
|
|
entry = entry_list[title_idx + 1]
|
|
title = entry_list[title_idx]
|
|
|
|
combined = "==[{0}]=={1}".format(title, entry)
|
|
new_entry = self.updateEntryBuildInfo(combined, build)
|
|
new_entry = new_entry.encode('utf-8')
|
|
|
|
it = re.finditer('\=\=\[(.+)\]\=\=', content)
|
|
entry_title = it.next()
|
|
while entry_title.group(1) != title:
|
|
entry_title = it.next()
|
|
next_title = it.next()
|
|
head = content[:entry_title.start()]
|
|
tail = content[next_title.start():]
|
|
update = head + new_entry + tail
|
|
|
|
# log.msg("wkl: Updating commit info YP_BUILDBRANCH=%s YP_CHASH=%s" %
|
|
# (buildbranch, chash))
|
|
|
|
return update
|
|
|
|
def updateBuild(self, build):
|
|
"""
|
|
Extract information about 'build' and update an entry in the wiki
|
|
|
|
@type build: buildbot.status.build.BuildStatus
|
|
"""
|
|
builder = build.getBuilder().getName()
|
|
buildid = str(build.getNumber())
|
|
reason = build.getReason()
|
|
blurb, entries = self.wiki.get_content(self.wiki_page)
|
|
if not blurb:
|
|
log.err("wkl: Unexpected content retrieved from wiki!")
|
|
return False
|
|
|
|
url = self.status.getURLForThing(build)
|
|
log_entries = []
|
|
logfmt = '[%s %s]'
|
|
for l in build.getLogs():
|
|
# Ignore logs for steps which succeeded
|
|
result, _ = l.getStep().getResults()
|
|
if result == SUCCESS:
|
|
continue
|
|
|
|
step_name = l.getStep().getName()
|
|
log_url = '%s/steps/%s/logs/%s' % (url,
|
|
step_name,
|
|
l.getName())
|
|
log_url = log_url.replace(' ', '%20')
|
|
log_entry = logfmt % (log_url, step_name)
|
|
log_entries.append(log_entry)
|
|
buildbranch = build.getProperty('branch').strip()
|
|
if not buildbranch or len(buildbranch) < 1:
|
|
buildbranch = "YP_BUILDBRANCH"
|
|
chash = build.getProperty('commit_poky').strip()
|
|
if not chash or len(chash) < 1 or chash == "HEAD":
|
|
chash = "YP_CHASH"
|
|
|
|
entry_list = re.split('\=\=\[(.+)\]\=\=', entries)
|
|
entry = ''
|
|
title = ''
|
|
# Start at the beginning of entry list and keep iterating until we find
|
|
# a title which looks ~right
|
|
trigger = "Triggerable(trigger_main-build"
|
|
for idx, entry in enumerate(entry_list):
|
|
# The matched title contents should always start with a http*
|
|
# schemed URI
|
|
if entry.startswith('http'):
|
|
# format of the title is:
|
|
# ==[url builder buildid - buildbranch commit_hash]==
|
|
title_components = entry.split(None, 6)
|
|
|
|
# For the primary, nightly, builder we can match on chash and
|
|
# buildbranch, otherwise we have to hope that the first
|
|
# triggered build with matching chash and tag
|
|
foundmatch = False
|
|
if buildbranch == title_components[4] \
|
|
and chash == title_components[5] \
|
|
and self.tag in entry_list[idx+1]:
|
|
foundmatch = True
|
|
elif trigger in reason \
|
|
and chash == title_components[5] \
|
|
and self.tag in entry_list[idx+1]:
|
|
foundmatch = True
|
|
|
|
if foundmatch:
|
|
entry = entry_list[idx+1]
|
|
title = entry_list[idx]
|
|
break
|
|
|
|
if not entry or not title:
|
|
errmsg = ("wkl: Failed to update entry for {0} couldn't find a "
|
|
"matching title for branch: {1} or hash: {2} "
|
|
"(reason was '{3}')")
|
|
log.err(errmsg.format(buildid, buildbranch, chash, reason))
|
|
return False
|
|
|
|
log_fmt = ''
|
|
logs = ''
|
|
new_entry = ''
|
|
if builder == 'nightly':
|
|
# for failures in nightly we just append extra entries to the
|
|
# bullet list pointing to the failure logs
|
|
if len(log_entries) > 0:
|
|
logs = '\n* '.join(log_entries) + '\n'
|
|
new_entry = '\n' + entry.strip() + '\n* ' + logs
|
|
else:
|
|
# We only update the buildlog for a nightly build if there
|
|
# are additional items to append to the log list.
|
|
return True
|
|
else:
|
|
# for non-nightly failures we create an entry in the list linking
|
|
# to the failed builder and indent the logs as a child bullet list
|
|
log_fmt = '\n* '
|
|
builderfmt = log_fmt
|
|
if self.isTriggered(build) or self.isNightlyRunning():
|
|
log_fmt = '\n** '
|
|
builderfmt = '\n* [%s %s] failed' % (url, builder)
|
|
|
|
if len(log_entries) > 0:
|
|
if self.isTriggered(build) or self.isNightlyRunning():
|
|
builderfmt = builderfmt + ': ' + log_fmt
|
|
logs = log_fmt.join(log_entries)
|
|
logs = logs + '\n'
|
|
new_entry = '\n' + entry.strip() + builderfmt + logs
|
|
|
|
summary = 'Updating entry with failures in %s' % builder
|
|
summary = summary + self.tag
|
|
|
|
new_entry = self.updateEntryBuildInfo(new_entry, build)
|
|
new_entry = new_entry.encode('utf-8')
|
|
|
|
# Find the point where the first entry's title starts and the second
|
|
# entry's title begins, then replace the text between those points
|
|
# with the newly generated entry.
|
|
it = re.finditer('\=\=\[(.+)\]\=\=', entries)
|
|
entry_title = it.next()
|
|
while entry_title.group(1) != title:
|
|
entry_title = it.next()
|
|
next_title = it.next()
|
|
head = entries[:entry_title.end()]
|
|
tail = entries[next_title.start():]
|
|
update = head + new_entry + tail
|
|
|
|
cookies = self.wiki.login()
|
|
if not cookies:
|
|
log.err("wkl: Failed to login to wiki")
|
|
return False
|
|
|
|
if not self.wiki.post_entry(self.wiki_page, blurb+update, summary,
|
|
cookies):
|
|
log.err("wkl: Failed to update entry for %s" % buildid)
|
|
return False
|
|
|
|
log.msg("wkl: Updating wikilog entry for %s" % buildid)
|
|
return True
|
|
|
|
def isNightlyRunning(self):
|
|
"""
|
|
Determine whether there's a nightly build in progress
|
|
"""
|
|
nightly = self.master.getBuilder("nightly")
|
|
if not nightly:
|
|
return False
|
|
build = nightly.getBuild(-1) # the most recent build
|
|
if not build:
|
|
return False
|
|
running = not build.isFinished()
|
|
return running
|
|
|
|
def isTriggered(self, build):
|
|
"""
|
|
build.isFinished() can return True when buildsteps triggered by
|
|
nightly are still running, therefore we provide a method to check
|
|
whether the 'build' was triggered by a nightly build.
|
|
|
|
@type build: buildbot.status.build.BuildStatus
|
|
"""
|
|
reason = build.getReason()
|
|
reason_list = reason.split(':', 1)
|
|
forcedby = reason_list[0].strip()
|
|
if forcedby.startswith("Triggerable"):
|
|
return True
|
|
return False
|