blob: 131e9cf62143df7b2f469a62143e53235666ca48 [file] [log] [blame]
# Copyright (C) 2019 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import calendar
import datetime
import json
import os
import time
from buildbot.util import service
from twisted.internet import defer
from twisted.internet import reactor
from twisted.internet.defer import succeed
from twisted.python import log
from twisted.web.client import Agent
from twisted.web.http_headers import Headers
from twisted.web.iweb import IBodyProducer
from zope.interface import implementer
@implementer(IBodyProducer)
class JSONProducer(object):
"""
Perform JSON asynchronously as to not lock the buildbot main event loop
"""
def __init__(self, data):
try:
self.body = json.dumps(data, default=self.json_serialize_datetime).encode('utf-8')
except TypeError:
self.body = ''
self.length = len(self.body)
def startProducing(self, consumer):
if self.body:
consumer.write(self.body)
return succeed(None)
def pauseProducing(self):
pass
def stopProducing(self):
pass
def json_serialize_datetime(self, obj):
"""
Serializing buildbot dates into UNIX epoch timestamps.
"""
if isinstance(obj, datetime.datetime):
return int(calendar.timegm(obj.timetuple()))
raise TypeError("Type %s not serializable" % type(obj))
class Events(service.BuildbotService):
EVENT_SERVER_ENDPOINT = b'https://ews.webkit.org/results/'
def __init__(self, master_hostname, type_prefix='', name='Events'):
"""
Initialize the Events Plugin. Sends data to event server on specific buildbot events.
:param type_prefix: [optional] prefix we want to add to the 'type' field on the json we send
to event server. (i.e. ews-build, where 'ews-' is the prefix.
:return: Events Object
"""
service.BuildbotService.__init__(self, name=name)
if type_prefix and not type_prefix.endswith("-"):
type_prefix += "-"
self.type_prefix = type_prefix
self.master_hostname = master_hostname
def sendData(self, data):
if os.getenv('EWS_API_KEY', None):
data['EWS_API_KEY'] = os.getenv('EWS_API_KEY')
agent = Agent(reactor)
body = JSONProducer(data)
agent.request(b'POST', self.EVENT_SERVER_ENDPOINT, Headers({'Content-Type': ['application/json']}), body)
def getBuilderName(self, build):
if not (build and 'properties' in build):
return ''
return build.get('properties').get('buildername')[0]
def getPatchID(self, build):
if not (build and 'properties' in build and 'patch_id' in build['properties']):
return None
return build.get('properties').get('patch_id')[0]
@defer.inlineCallbacks
def buildStarted(self, key, build):
if not build.get('properties'):
build['properties'] = yield self.master.db.builds.getBuildProperties(build.get('buildid'))
builder = yield self.master.db.builders.getBuilder(build.get('builderid'))
builder_display_name = builder.get('description')
data = {
"type": self.type_prefix + "build",
"status": "started",
"hostname": self.master_hostname,
"patch_id": self.getPatchID(build),
"build_id": build.get('buildid'),
"builder_id": build.get('builderid'),
"number": build.get('number'),
"result": build.get('results'),
"started_at": build.get('started_at'),
"complete_at": build.get('complete_at'),
"state_string": build.get('state_string'),
"builder_name": self.getBuilderName(build),
"builder_display_name": builder_display_name,
}
self.sendData(data)
@defer.inlineCallbacks
def buildFinished(self, key, build):
if not build.get('properties'):
build['properties'] = yield self.master.db.builds.getBuildProperties(build.get('buildid'))
if not build.get('steps'):
build['steps'] = yield self.master.db.steps.getSteps(build.get('buildid'))
builder = yield self.master.db.builders.getBuilder(build.get('builderid'))
builder_display_name = builder.get('description')
data = {
"type": self.type_prefix + "build",
"status": "finished",
"hostname": self.master_hostname,
"patch_id": self.getPatchID(build),
"build_id": build.get('buildid'),
"builder_id": build.get('builderid'),
"number": build.get('number'),
"result": build.get('results'),
"started_at": build.get('started_at'),
"complete_at": build.get('complete_at'),
"state_string": build.get('state_string'),
"builder_name": self.getBuilderName(build),
"builder_display_name": builder_display_name,
"steps": build.get('steps'),
}
self.sendData(data)
def stepStarted(self, key, step):
state_string = step.get('state_string')
if state_string == 'pending':
state_string = 'Running {}'.format(step.get('name'))
data = {
"type": self.type_prefix + "step",
"status": "started",
"hostname": self.master_hostname,
"step_id": step.get('stepid'),
"build_id": step.get('buildid'),
"result": step.get('results'),
"state_string": state_string,
"started_at": step.get('started_at'),
"complete_at": step.get('complete_at'),
}
self.sendData(data)
def stepFinished(self, key, step):
data = {
"type": self.type_prefix + "step",
"status": "finished",
"hostname": self.master_hostname,
"step_id": step.get('stepid'),
"build_id": step.get('buildid'),
"result": step.get('results'),
"state_string": step.get('state_string'),
"started_at": step.get('started_at'),
"complete_at": step.get('complete_at'),
}
self.sendData(data)
@defer.inlineCallbacks
def startService(self):
yield service.BuildbotService.startService(self)
startConsuming = self.master.mq.startConsuming
self._buildStartedConsumer = yield startConsuming(self.buildStarted, ('builds', None, 'new'))
self._buildCompleteConsumer = yield startConsuming(self.buildFinished, ('builds', None, 'finished'))
self._stepStartedConsumer = yield startConsuming(self.stepStarted, ('steps', None, 'started'))
self._stepFinishedConsumer = yield startConsuming(self.stepFinished, ('steps', None, 'finished'))
def stopService(self):
self._buildStartedConsumer.stopConsuming()
self._buildCompleteConsumer.stopConsuming()
self._stepStartedConsumer.stopConsuming()
self._stepFinishedConsumer.stopConsuming()