Get to submit to treeherder
This commit is contained in:
Родитель
060dd2a2d5
Коммит
661bf80cf4
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
// Add field 'treeherder' to the awfy_run database.
|
||||||
|
// And mark all previous runs supposedly submitted to treeherder
|
||||||
|
|
||||||
|
$migrate = function() {
|
||||||
|
mysql_query("ALTER TABLE `awfy_run` ADD `treeherder` BOOLEAN NOT NULL;") or die(mysql_error());
|
||||||
|
mysql_query("UPDATE awfy_run SET `treeherder` = 1 WHERE status != 0;") or die(mysql_error());
|
||||||
|
};
|
||||||
|
|
||||||
|
$rollback = function() {
|
||||||
|
mysql_query("ALTER TABLE `awfy_run` DROP `treeherder`;") or die(mysql_error());
|
||||||
|
};
|
|
@ -1,73 +0,0 @@
|
||||||
# vim: set ts=4 sw=4 tw=99 et:
|
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
try:
|
|
||||||
import MySQLdb as mdb
|
|
||||||
except:
|
|
||||||
import mysqldb as mdb
|
|
||||||
try:
|
|
||||||
import ConfigParser
|
|
||||||
except:
|
|
||||||
import configparser as ConfigParser
|
|
||||||
|
|
||||||
db = None
|
|
||||||
version = None
|
|
||||||
path = None
|
|
||||||
|
|
||||||
|
|
||||||
queries = 0
|
|
||||||
class DB:
|
|
||||||
def __init__(self, host, user, pw, name):
|
|
||||||
self.host = host
|
|
||||||
self.user = user
|
|
||||||
self.pw = pw
|
|
||||||
self.name = name
|
|
||||||
self.connect()
|
|
||||||
def connect(self):
|
|
||||||
if self.host[0] == '/':
|
|
||||||
self.db = mdb.connect(unix_socket=self.host, user=self.user, passwd=self.pw,
|
|
||||||
db=self.name, use_unicode=True)
|
|
||||||
else:
|
|
||||||
self.db = mdb.connect(self.host, self.user, self.pw, self.name, use_unicode=True)
|
|
||||||
def cursor(self):
|
|
||||||
return DBCursor(self.db.cursor())
|
|
||||||
def commit(self):
|
|
||||||
return self.db.commit()
|
|
||||||
class DBCursor:
|
|
||||||
def __init__(self, cursor):
|
|
||||||
self.cursor = cursor
|
|
||||||
def execute(self, sql, data=None):
|
|
||||||
global queries
|
|
||||||
queries+=1
|
|
||||||
exe = self.cursor.execute(sql, data);
|
|
||||||
self.description = self.cursor.description
|
|
||||||
self.lastrowid = self.cursor.lastrowid
|
|
||||||
self.rowcount = self.cursor.rowcount
|
|
||||||
return exe
|
|
||||||
def fetchone(self):
|
|
||||||
return self.cursor.fetchone();
|
|
||||||
def fetchall(self):
|
|
||||||
return self.cursor.fetchall();
|
|
||||||
|
|
||||||
def Startup():
|
|
||||||
global db, version, path
|
|
||||||
config = ConfigParser.RawConfigParser()
|
|
||||||
config.read("/etc/awfy-server.config")
|
|
||||||
|
|
||||||
host = config.get('mysql', 'host')
|
|
||||||
user = config.get('mysql', 'user')
|
|
||||||
pw = config.get('mysql', 'pass')
|
|
||||||
name = config.get('mysql', 'db_name')
|
|
||||||
|
|
||||||
db = DB(host, user, pw, name)
|
|
||||||
c = db.cursor()
|
|
||||||
c.execute("SELECT `value` FROM awfy_config WHERE `key` = 'version'")
|
|
||||||
row = c.fetchone()
|
|
||||||
version = int(row[0])
|
|
||||||
|
|
||||||
path = config.get('general', 'data_folder')
|
|
||||||
|
|
||||||
Startup()
|
|
||||||
|
|
|
@ -3,11 +3,14 @@
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
import awfy
|
from optparse import OptionParser
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
sys.path.append("../server")
|
||||||
|
import awfy
|
||||||
import tables
|
import tables
|
||||||
from optparse import OptionParser
|
|
||||||
|
|
||||||
parser = OptionParser(usage="usage: %prog [options]")
|
parser = OptionParser(usage="usage: %prog [options]")
|
||||||
parser.add_option("-n", "--non-existing", dest="nonexistonly", action="store_true", default=False,
|
parser.add_option("-n", "--non-existing", dest="nonexistonly", action="store_true", default=False,
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append("../server")
|
||||||
import awfy
|
import awfy
|
||||||
import tables
|
import tables
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
import awfy
|
from optparse import OptionParser
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import tables
|
|
||||||
|
|
||||||
from optparse import OptionParser
|
sys.path.append("../server")
|
||||||
|
import awfy
|
||||||
|
import tables
|
||||||
|
|
||||||
parser = OptionParser(usage="usage: %prog [options]")
|
parser = OptionParser(usage="usage: %prog [options]")
|
||||||
parser.add_option( "--dry-run", dest="dryrun", action="store_true", default=False,
|
parser.add_option( "--dry-run", dest="dryrun", action="store_true", default=False,
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append("../server")
|
||||||
import awfy
|
import awfy
|
||||||
|
|
||||||
c = awfy.db.cursor()
|
c = awfy.db.cursor()
|
||||||
|
|
|
@ -7,4 +7,9 @@ db_name = ???
|
||||||
[general]
|
[general]
|
||||||
data_folder = /home/awfy
|
data_folder = /home/awfy
|
||||||
machine_timeout = 480 ; 8 hours (480 minutes)
|
machine_timeout = 480 ; 8 hours (480 minutes)
|
||||||
|
slack_webhook = ???
|
||||||
|
|
||||||
|
[treeherder]
|
||||||
|
host = ???
|
||||||
|
user = ???
|
||||||
|
secret = ???
|
||||||
|
|
|
@ -15,6 +15,9 @@ except:
|
||||||
db = None
|
db = None
|
||||||
version = None
|
version = None
|
||||||
path = None
|
path = None
|
||||||
|
th_host = None
|
||||||
|
th_user = None
|
||||||
|
th_secret = None
|
||||||
|
|
||||||
|
|
||||||
queries = 0
|
queries = 0
|
||||||
|
@ -52,7 +55,7 @@ class DBCursor:
|
||||||
return self.cursor.fetchall();
|
return self.cursor.fetchall();
|
||||||
|
|
||||||
def Startup():
|
def Startup():
|
||||||
global db, version, path
|
global db, version, path, th_host, th_user, th_secret
|
||||||
config = ConfigParser.RawConfigParser()
|
config = ConfigParser.RawConfigParser()
|
||||||
config.read("/etc/awfy-server.config")
|
config.read("/etc/awfy-server.config")
|
||||||
|
|
||||||
|
@ -69,5 +72,9 @@ def Startup():
|
||||||
|
|
||||||
path = config.get('general', 'data_folder')
|
path = config.get('general', 'data_folder')
|
||||||
|
|
||||||
Startup()
|
if config.has_section('treeherder'):
|
||||||
|
th_host = config.get('treeherder', 'host')
|
||||||
|
th_user = config.get('treeherder', 'user')
|
||||||
|
th_secret = config.get('treeherder', 'secret')
|
||||||
|
|
||||||
|
Startup()
|
||||||
|
|
|
@ -28,6 +28,13 @@ def camelcase(string):
|
||||||
class DBTable(object):
|
class DBTable(object):
|
||||||
globalcache = {}
|
globalcache = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def FromId(class_, id):
|
||||||
|
obj = class_(row[0])
|
||||||
|
if obj.exists():
|
||||||
|
return obj
|
||||||
|
return None
|
||||||
|
|
||||||
def __init__(self, id):
|
def __init__(self, id):
|
||||||
self.id = int(id)
|
self.id = int(id)
|
||||||
self.initialized = False
|
self.initialized = False
|
||||||
|
@ -142,6 +149,19 @@ class Run(DBTable):
|
||||||
def table():
|
def table():
|
||||||
return "awfy_run"
|
return "awfy_run"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromSortOrder(machine_id, sort_order):
|
||||||
|
c = awfy.db.cursor()
|
||||||
|
c.execute("SELECT id \
|
||||||
|
FROM awfy_run \
|
||||||
|
WHERE machine = %s AND \
|
||||||
|
sort_order = %s", (machine_id, sort_order))
|
||||||
|
rows = c.fetchall()
|
||||||
|
if len(rows) == 0:
|
||||||
|
return None
|
||||||
|
assert len(rows) == 1
|
||||||
|
return Run(rows[0][0])
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
if self.initialized:
|
if self.initialized:
|
||||||
return
|
return
|
||||||
|
@ -348,6 +368,19 @@ class Build(DBTable):
|
||||||
def table():
|
def table():
|
||||||
return "awfy_build"
|
return "awfy_build"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromRunAndMode(run_id, mode_id):
|
||||||
|
c = awfy.db.cursor()
|
||||||
|
c.execute("SELECT id \
|
||||||
|
FROM awfy_build \
|
||||||
|
WHERE run_id = %s AND \
|
||||||
|
mode_id = %s", (run_id, mode_id))
|
||||||
|
rows = c.fetchall()
|
||||||
|
if len(rows) == 0:
|
||||||
|
return None
|
||||||
|
assert len(rows) == 1
|
||||||
|
return Build(rows[0][0])
|
||||||
|
|
||||||
def getScores(self):
|
def getScores(self):
|
||||||
scores = []
|
scores = []
|
||||||
c = awfy.db.cursor()
|
c = awfy.db.cursor()
|
||||||
|
@ -591,6 +624,29 @@ class Score(RegressionTools):
|
||||||
def table():
|
def table():
|
||||||
return "awfy_score"
|
return "awfy_score"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromBuildAndSuite(build_id, suite_version_id):
|
||||||
|
c = awfy.db.cursor()
|
||||||
|
c.execute("SELECT id \
|
||||||
|
FROM awfy_score \
|
||||||
|
WHERE build_id = %s AND \
|
||||||
|
suite_version_id = %s", (build_id, suite_version_id))
|
||||||
|
rows = c.fetchall()
|
||||||
|
if len(rows) == 0:
|
||||||
|
return None
|
||||||
|
assert len(rows) == 1
|
||||||
|
return Score(rows[0][0])
|
||||||
|
|
||||||
|
def getBreakdowns(self):
|
||||||
|
c = awfy.db.cursor()
|
||||||
|
c.execute("SELECT awfy_breakdown.id \
|
||||||
|
FROM awfy_breakdown \
|
||||||
|
WHERE score_id = %s", (self.id,))
|
||||||
|
breakdowns = []
|
||||||
|
for row in c.fetchall():
|
||||||
|
breakdowns.append(Breakdown(row[0]))
|
||||||
|
return breakdowns
|
||||||
|
|
||||||
def sane(self):
|
def sane(self):
|
||||||
if self.get("suite_version_id") == -1:
|
if self.get("suite_version_id") == -1:
|
||||||
return False
|
return False
|
|
@ -0,0 +1,12 @@
|
||||||
|
[{
|
||||||
|
"machine": 28,
|
||||||
|
"modes": [{
|
||||||
|
"mode": "jmim",
|
||||||
|
"repo": "mozilla-inbound",
|
||||||
|
"platform": ["linux", "linux32", "x86"],
|
||||||
|
"tier": 2,
|
||||||
|
"job_symbol": "shell",
|
||||||
|
"job_name": "shell",
|
||||||
|
"enabled": true
|
||||||
|
}]
|
||||||
|
}]
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"machine": {
|
||||||
|
"description": "The id of the machine in the db",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"modes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["mode","repo","tier","job_symbol","job_name","platform","enabled"],
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"description": "the mode name in the db",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"repo": {
|
||||||
|
"description": "The repo this dataset is testing. E.g. mozilla-inbound",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tier": {
|
||||||
|
"description": "On a scale of 1-3 how important the data is",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 3
|
||||||
|
},
|
||||||
|
"job_symbol": {
|
||||||
|
"description": "The symbol used to report on treeherder",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"job_name": {
|
||||||
|
"description": "The name used to report on treeherder",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"description": "An array explaining the platform. [OS, OS+bits, CPU]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"minItems": 3,
|
||||||
|
"maxItems": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from urlparse import urljoin, urlparse
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
from thclient import TreeherderClient, TreeherderJob, TreeherderJobCollection
|
||||||
|
except:
|
||||||
|
print "run 'sudo pip install treeherder-client' to install the needed libraries"
|
||||||
|
exit()
|
||||||
|
|
||||||
|
RESULTSET_FRAGMENT = 'api/project/{repository}/resultset/?revision={revision}'
|
||||||
|
JOB_FRAGMENT = '/#/jobs?repo={repository}&revision={revision}'
|
||||||
|
BUILD_STATES = ['running', 'completed']
|
||||||
|
|
||||||
|
logging.basicConfig(format='%(asctime)s %(levelname)s | %(message)s', datefmt='%H:%M:%S')
|
||||||
|
logger = logging.getLogger('mozmill-ci')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
class Submission(object):
|
||||||
|
"""Class for submitting reports to Treeherder."""
|
||||||
|
|
||||||
|
def __init__(self, repository, revision, settings,
|
||||||
|
treeherder_url=None, treeherder_client_id=None, treeherder_secret=None):
|
||||||
|
"""Creates new instance of the submission class.
|
||||||
|
|
||||||
|
:param repository: Name of the repository the build has been built from.
|
||||||
|
:param revision: Changeset of the repository the build has been built from.
|
||||||
|
:param settings: Settings for the Treeherder job as retrieved from the config file.
|
||||||
|
:param treeherder_url: URL of the Treeherder instance.
|
||||||
|
:param treeherder_client_id: The client ID necessary for the Hawk authentication.
|
||||||
|
:param treeherder_secret: The secret key necessary for the Hawk authentication.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.repository = repository
|
||||||
|
self.revision = revision
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
self._job_details = []
|
||||||
|
|
||||||
|
self.url = treeherder_url
|
||||||
|
self.client_id = treeherder_client_id
|
||||||
|
self.secret = treeherder_secret
|
||||||
|
|
||||||
|
if not self.client_id or not self.secret and self.url != "mock":
|
||||||
|
raise ValueError('The client_id and secret for Treeherder must be set.')
|
||||||
|
|
||||||
|
def _get_treeherder_platform(self):
|
||||||
|
"""Returns the Treeherder equivalent platform identifier of the current platform."""
|
||||||
|
|
||||||
|
#Todo
|
||||||
|
return ('linux', 'linux64', 'x86_64')
|
||||||
|
|
||||||
|
def create_job(self, data=None, **kwargs):
|
||||||
|
"""Creates a new instance of a Treeherder job for submission.
|
||||||
|
|
||||||
|
:param data: Job data to use for initilization, e.g. from a previous submission, optional
|
||||||
|
:param kwargs: Dictionary of necessary values to build the job details. The
|
||||||
|
properties correlate to the placeholders in config.py.
|
||||||
|
|
||||||
|
"""
|
||||||
|
data = data or {}
|
||||||
|
|
||||||
|
job = TreeherderJob(data=data)
|
||||||
|
|
||||||
|
# If no data is available we have to set all properties
|
||||||
|
if not data:
|
||||||
|
job.add_job_guid(str(uuid.uuid4()))
|
||||||
|
job.add_tier(self.settings['treeherder']['tier'])
|
||||||
|
|
||||||
|
job.add_product_name('firefox')
|
||||||
|
|
||||||
|
job.add_project(self.repository)
|
||||||
|
job.add_revision_hash(self.retrieve_revision_hash())
|
||||||
|
|
||||||
|
# Add platform and build information
|
||||||
|
job.add_machine(socket.getfqdn())
|
||||||
|
platform = self._get_treeherder_platform()
|
||||||
|
job.add_machine_info(*platform)
|
||||||
|
job.add_build_info(*platform)
|
||||||
|
|
||||||
|
# TODO debug or others?
|
||||||
|
job.add_option_collection({'opt': True})
|
||||||
|
|
||||||
|
# TODO: Add e10s group once we run those tests
|
||||||
|
job.add_group_name(self.settings['treeherder']['group_name'].format(**kwargs))
|
||||||
|
job.add_group_symbol(self.settings['treeherder']['group_symbol'].format(**kwargs))
|
||||||
|
|
||||||
|
# Bug 1174973 - for now we need unique job names even in different groups
|
||||||
|
job.add_job_name(self.settings['treeherder']['job_name'].format(**kwargs))
|
||||||
|
job.add_job_symbol(self.settings['treeherder']['job_symbol'].format(**kwargs))
|
||||||
|
|
||||||
|
job.add_start_timestamp(int(time.time()))
|
||||||
|
|
||||||
|
# Bug 1175559 - Workaround for HTTP Error
|
||||||
|
job.add_end_timestamp(0)
|
||||||
|
|
||||||
|
return job
|
||||||
|
|
||||||
|
def retrieve_revision_hash(self):
|
||||||
|
"""Retrieves the unique hash for the current revision."""
|
||||||
|
if not self.url:
|
||||||
|
raise ValueError('URL for Treeherder is missing.')
|
||||||
|
|
||||||
|
lookup_url = urljoin(self.url,
|
||||||
|
RESULTSET_FRAGMENT.format(repository=self.repository,
|
||||||
|
revision=self.revision))
|
||||||
|
|
||||||
|
if self.url == "mock":
|
||||||
|
logger.info('Pretend to get revision hash from: {}'.format(lookup_url))
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info('Getting revision hash from: {}'.format(lookup_url))
|
||||||
|
|
||||||
|
response = requests.get(lookup_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
if not response.json():
|
||||||
|
raise ValueError('Unable to determine revision hash for {}. '
|
||||||
|
'Perhaps it has not been ingested by '
|
||||||
|
'Treeherder?'.format(self.revision))
|
||||||
|
|
||||||
|
return response.json()['results'][0]['revision_hash']
|
||||||
|
|
||||||
|
def submit(self, job):
|
||||||
|
"""Submit the job to treeherder.
|
||||||
|
|
||||||
|
:param job: Treeherder job instance to use for submission.
|
||||||
|
|
||||||
|
"""
|
||||||
|
job.add_submit_timestamp(int(time.time()))
|
||||||
|
|
||||||
|
# We can only submit job info once, so it has to be done in completed
|
||||||
|
if self._job_details:
|
||||||
|
job.add_artifact('Job Info', 'json', {'job_details': self._job_details})
|
||||||
|
|
||||||
|
job_collection = TreeherderJobCollection()
|
||||||
|
job_collection.add(job)
|
||||||
|
|
||||||
|
logger.info('Sending results to Treeherder: {}'.format(job_collection.to_json()))
|
||||||
|
if self.url == 'mock':
|
||||||
|
logger.info('Pretending to submit job')
|
||||||
|
return
|
||||||
|
|
||||||
|
url = urlparse(self.url)
|
||||||
|
client = TreeherderClient(protocol=url.scheme, host=url.hostname,
|
||||||
|
client_id=self.client_id, secret=self.secret)
|
||||||
|
client.post_collection(self.repository, job_collection)
|
||||||
|
|
||||||
|
logger.info('Results are available to view at: {}'.format(
|
||||||
|
urljoin(self.url,
|
||||||
|
JOB_FRAGMENT.format(repository=self.repository,
|
||||||
|
revision=self.revision))))
|
||||||
|
|
||||||
|
def submit_running_job(self, job):
|
||||||
|
"""Submit job as state running.
|
||||||
|
|
||||||
|
:param job: Treeherder job instance to use for submission.
|
||||||
|
|
||||||
|
"""
|
||||||
|
job.add_state('running')
|
||||||
|
self.submit(job)
|
||||||
|
|
||||||
|
def submit_completed_job(self, job, perfdata, state="success"):
|
||||||
|
"""Submit job as state completed.
|
||||||
|
|
||||||
|
:param job: Treeherder job instance to use for submission.
|
||||||
|
:param state: success, testfailed, busted, skipped, exception, retry, usercancel
|
||||||
|
:param uploaded_logs: List of uploaded logs to reference in the job.
|
||||||
|
|
||||||
|
"""
|
||||||
|
job.add_state('completed')
|
||||||
|
job.add_result(state)
|
||||||
|
|
||||||
|
jsondata = json.dumps({'performance_data': perfdata})
|
||||||
|
job.add_artifact('performance_data', 'json', jsondata)
|
||||||
|
|
||||||
|
job.add_end_timestamp(int(time.time()))
|
||||||
|
|
||||||
|
self.submit(job)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_log_files(guid, logs,
|
||||||
|
bucket_name=None, access_key_id=None, access_secret_key=None):
|
||||||
|
"""Upload all specified logs to Amazon S3.
|
||||||
|
|
||||||
|
:param guid: Unique ID which is used as subfolder name for all log files.
|
||||||
|
:param logs: List of log files to upload.
|
||||||
|
:param bucket_name: Name of the S3 bucket.
|
||||||
|
:param access_key_id: Client ID used for authentication.
|
||||||
|
:param access_secret_key: Secret key for authentication.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If no AWS credentials are given we don't upload anything.
|
||||||
|
if not bucket_name:
|
||||||
|
logger.info('No AWS Bucket name specified - skipping upload of artifacts.')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
s3_bucket = S3Bucket(bucket_name, access_key_id=access_key_id,
|
||||||
|
access_secret_key=access_secret_key)
|
||||||
|
|
||||||
|
uploaded_logs = {}
|
||||||
|
|
||||||
|
for log in logs:
|
||||||
|
try:
|
||||||
|
if os.path.isfile(logs[log]):
|
||||||
|
remote_path = '{dir}/{filename}'.format(dir=str(guid),
|
||||||
|
filename=os.path.basename(log))
|
||||||
|
url = s3_bucket.upload(logs[log], remote_path)
|
||||||
|
|
||||||
|
uploaded_logs.update({log: {'path': logs[log], 'url': url}})
|
||||||
|
logger.info('Uploaded {path} to {url}'.format(path=logs[log], url=url))
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Failure uploading "{path}" to S3'.format(path=logs[log]))
|
||||||
|
|
||||||
|
return uploaded_logs
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
from submission import Submission
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append("../server")
|
||||||
|
import awfy
|
||||||
|
import tables
|
||||||
|
|
||||||
|
def first(gen):
|
||||||
|
return list(gen)[0]
|
||||||
|
|
||||||
|
class Submitter(object):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
Submit the given data
|
||||||
|
"""
|
||||||
|
def submit(self, revision, data, mode_info):
|
||||||
|
repo = mode_info["repo"]
|
||||||
|
settings = {
|
||||||
|
"treeherder": {
|
||||||
|
'group_symbol': 'AWFY',
|
||||||
|
'group_name': 'AWFY',
|
||||||
|
'job_name': mode_info["job_name"],
|
||||||
|
'job_symbol': mode_info["job_symbol"],
|
||||||
|
"tier": mode_info["tier"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th = Submission(repo, revision,
|
||||||
|
treeherder_url = awfy.th_host,
|
||||||
|
treeherder_client_id = awfy.th_user,
|
||||||
|
treeherder_secret = awfy.th_secret,
|
||||||
|
settings = settings)
|
||||||
|
|
||||||
|
job = th.create_job(None)
|
||||||
|
th.submit_completed_job(job, data)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Takes all scores/subscores from a build and submit the data to treeherder.
|
||||||
|
"""
|
||||||
|
def submitBuild(self, build):
|
||||||
|
revision = build.get("cset")
|
||||||
|
machine_id = build.get("run").get("machine_id")
|
||||||
|
mode_symbol = build.get("mode").get("mode")
|
||||||
|
perfdata = []
|
||||||
|
|
||||||
|
mode_info = config.mode_info(machine_id, mode_symbol)
|
||||||
|
if not mode_info:
|
||||||
|
print "Couldn't submit", revision, "with mode", mode_symbol
|
||||||
|
print "No data found in the config file about how to submit it"
|
||||||
|
return
|
||||||
|
|
||||||
|
scores = build.getScores()
|
||||||
|
for score in scores:
|
||||||
|
if not score.get("suite_version") or not score.get("suite_version").exists():
|
||||||
|
continue
|
||||||
|
suite_version = score.get("suite_version")
|
||||||
|
perfdata.append({
|
||||||
|
"name": suite_version.get("name"),
|
||||||
|
"score": score.get("score"),
|
||||||
|
"lowerIsBetter": suite_version.get("suite").get("better_direction") == -1,
|
||||||
|
"subscores": {}
|
||||||
|
})
|
||||||
|
for breakdown in score.getBreakdowns():
|
||||||
|
if not breakdown.get("suite_test") or not breakdown.get("suite_test").exists():
|
||||||
|
continue
|
||||||
|
suite_test = breakdown.get("suite_test")
|
||||||
|
perfdata[-1]["subscores"][suite_test.get("name")] = breakdown.get("score")
|
||||||
|
|
||||||
|
data = self.transform(perfdata)
|
||||||
|
|
||||||
|
self.submit(revision, data, mode_info)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Takes all builds from a run and submit the enabled ones to treeherder.
|
||||||
|
"""
|
||||||
|
def submitRun(self, run):
|
||||||
|
# Annonate run that it was forwared to treeherder.
|
||||||
|
run.update({"treeherder": 1})
|
||||||
|
awfy.db.commit()
|
||||||
|
|
||||||
|
# Send the data.
|
||||||
|
modes = config.modes(run.get("machine_id"))
|
||||||
|
for mode in modes:
|
||||||
|
mode = first(tables.Mode.where({"mode": mode}))
|
||||||
|
build = tables.Build.fromRunAndMode(run.id, mode.id)
|
||||||
|
self.submitBuild(build)
|
||||||
|
|
||||||
|
"""
|
||||||
|
transforms the intermediate representation of benchmark results pulled from the DB
|
||||||
|
into the canonical format needed by treeherder/perfherder
|
||||||
|
"""
|
||||||
|
def transform(self, tests):
|
||||||
|
data = {
|
||||||
|
"framework": {
|
||||||
|
"name": "awfy"
|
||||||
|
},
|
||||||
|
"suites": []
|
||||||
|
}
|
||||||
|
for test in tests:
|
||||||
|
testdata = {
|
||||||
|
"name": test["name"],
|
||||||
|
"value": float(test["score"]),
|
||||||
|
"subtests": [],
|
||||||
|
"lowerIsBetter": bool(test["lowerIsBetter"])
|
||||||
|
}
|
||||||
|
if "subscores" not in test:
|
||||||
|
test["subscores"] = []
|
||||||
|
for subtest in test["subscores"]:
|
||||||
|
subtestdata = {
|
||||||
|
"lowerIsBetter": bool(test["lowerIsBetter"]),
|
||||||
|
"name": subtest,
|
||||||
|
"value": float(test["subscores"][subtest])
|
||||||
|
}
|
||||||
|
testdata["subtests"].append(subtestdata)
|
||||||
|
data["suites"].append(testdata)
|
||||||
|
return data
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
def __init__(self, filename):
|
||||||
|
fp = open(filename)
|
||||||
|
self.data = json.load(fp)
|
||||||
|
fp.close()
|
||||||
|
|
||||||
|
def modes(self, machine_id):
|
||||||
|
for machine_data in self.data:
|
||||||
|
if str(machine_data["machine"]) != str(machine_id):
|
||||||
|
continue
|
||||||
|
return [mode["mode"] for mode in machine_data["modes"] if mode["enabled"]]
|
||||||
|
return []
|
||||||
|
def mode_info(self, machine_id, mode_symbol):
|
||||||
|
for machine_data in self.data:
|
||||||
|
if str(machine_data["machine"]) != str(machine_id):
|
||||||
|
continue
|
||||||
|
for mode in machine_data["modes"]:
|
||||||
|
if mode["mode"] != mode_symbol:
|
||||||
|
continue
|
||||||
|
return mode
|
||||||
|
return None
|
||||||
|
def validate(self, schema):
|
||||||
|
from jsonschema import validate
|
||||||
|
fp = open(schema)
|
||||||
|
schema = json.load(fp)
|
||||||
|
fp.close()
|
||||||
|
validate(self.data, schema)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
config = Config("config.json")
|
||||||
|
config.validate("config.schema")
|
||||||
|
|
||||||
|
submitter = Submitter()
|
||||||
|
for run in tables.Run.where({"status": 1, "treeherder": 0}):
|
||||||
|
submitter.submitRun(run)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче