# 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 logging
import redo
import requests
from gecko_taskgraph.util.scriptworker
import (
BALROG_SCOPE_ALIAS_TO_PROJECT,
BALROG_SERVER_SCOPES,
)
logger = logging.getLogger(__name__)
PLATFORM_RENAMES = {
"windows2012-32":
"win32",
"windows2012-64":
"win64",
"windows2012-aarch64":
"win64-aarch64",
"osx-cross":
"macosx64",
"osx":
"macosx64",
}
BALROG_PLATFORM_MAP = {
"linux": [
"Linux_x86-gcc3"],
"linux32": [
"Linux_x86-gcc3"],
"linux64": [
"Linux_x86_64-gcc3"],
"linux64-aarch64": [
"Linux_aarch64-gcc3"],
"linux64-asan-reporter": [
"Linux_x86_64-gcc3-asan"],
"macosx64": [
"Darwin_x86_64-gcc3-u-i386-x86_64",
"Darwin_x86-gcc3-u-i386-x86_64",
"Darwin_aarch64-gcc3",
"Darwin_x86-gcc3",
"Darwin_x86_64-gcc3",
],
"win32": [
"WINNT_x86-msvc",
"WINNT_x86-msvc-x86",
"WINNT_x86-msvc-x64"],
"win64": [
"WINNT_x86_64-msvc",
"WINNT_x86_64-msvc-x64"],
"win64-asan-reporter": [
"WINNT_x86_64-msvc-x64-asan"],
"win64-aarch64": [
"WINNT_aarch64-msvc-aarch64",
],
}
FTP_PLATFORM_MAP = {
"Darwin_x86-gcc3":
"mac",
"Darwin_x86-gcc3-u-i386-x86_64":
"mac",
"Darwin_x86_64-gcc3":
"mac",
"Darwin_x86_64-gcc3-u-i386-x86_64":
"mac",
"Darwin_aarch64-gcc3":
"mac",
"Linux_x86-gcc3":
"linux-i686",
"Linux_x86_64-gcc3":
"linux-x86_64",
"Linux_aarch64-gcc3":
"linux-aarch64",
"Linux_x86_64-gcc3-asan":
"linux-x86_64-asan-reporter",
"WINNT_x86_64-msvc-x64-asan":
"win64-asan-reporter",
"WINNT_x86-msvc":
"win32",
"WINNT_x86-msvc-x64":
"win32",
"WINNT_x86-msvc-x86":
"win32",
"WINNT_x86_64-msvc":
"win64",
"WINNT_x86_64-msvc-x64":
"win64",
"WINNT_aarch64-msvc-aarch64":
"win64-aarch64",
}
def get_balrog_platform_name(platform):
"""Convert build platform names into balrog platform names.
Remove known values instead to catch aarch64
and other platforms
that may be added.
"""
removals = [
"-devedition",
"-shippable"]
for remove
in removals:
platform = platform.replace(remove,
"")
return PLATFORM_RENAMES.get(platform, platform)
def _sanitize_platform(platform):
platform = get_balrog_platform_name(platform)
return BALROG_PLATFORM_MAP[platform][0]
def get_builds(release_history, platform, locale):
"""Examine cached balrog release history and return the list of
builds we need to generate diffs
from"""
platform = _sanitize_platform(platform)
return release_history.get(platform, {}).get(locale, {})
def get_partials_artifacts_from_params(release_history, platform, locale):
platform = _sanitize_platform(platform)
return [
(artifact, details.get(
"previousVersion",
None))
for artifact, details
in release_history.get(platform, {})
.get(locale, {})
.items()
]
def get_partials_info_from_params(release_history, platform, locale):
platform = _sanitize_platform(platform)
artifact_map = {}
for k
in release_history.get(platform, {}).get(locale, {}):
details = release_history[platform][locale][k]
attributes = (
"buildid",
"previousBuildNumber",
"previousVersion")
artifact_map[k] = {
attr: details[attr]
for attr
in attributes
if attr
in details
}
return artifact_map
def _retry_on_http_errors(url, verify, params, errors):
if params:
params_str =
"&".join(
"=".join([k, str(v)])
for k, v
in params.items())
else:
params_str =
""
logger.info(
"Connecting to %s?%s", url, params_str)
for _
in redo.retrier(sleeptime=5, max_sleeptime=30, attempts=10):
try:
req = requests.get(url, verify=verify, params=params, timeout=10)
req.raise_for_status()
return req
except requests.HTTPError
as e:
if e.response.status_code
in errors:
logger.exception(
"Got HTTP %s trying to reach %s", e.response.status_code, url
)
else:
raise
else:
raise Exception(f
"Cannot connect to {url}!")
def get_sorted_releases(product, branch):
"""Returns a list of release names from Balrog.
:param product: product name, AKA appName
:param branch: branch name, e.g. mozilla-central
:
return: a sorted list of release names, most recent first.
"""
url = f
"{_get_balrog_api_root(branch)}/releases"
params = {
"product": product,
# Adding -nightly-2 (2 stands for the beginning of build ID
# based on date) should filter out release and latest blobs.
# This should be changed to -nightly-3 in 3000 ;)
"name_prefix": f
"{product}-{branch}-nightly-2",
"names_only":
True,
}
req = _retry_on_http_errors(url=url, verify=
True, params=params, errors=[500, 502])
releases = req.json()[
"names"]
releases = sorted(releases, reverse=
True)
return releases
def get_release_builds(release, branch):
url = f
"{_get_balrog_api_root(branch)}/releases/{release}"
req = _retry_on_http_errors(url=url, verify=
True, params=
None, errors=[500, 502])
return req.json()
def _get_balrog_api_root(branch):
# Query into the scopes scriptworker uses to make sure we check against the same balrog server
# That our jobs would use.
scope =
None
for alias, projects
in BALROG_SCOPE_ALIAS_TO_PROJECT:
if branch
in projects
and alias
in BALROG_SERVER_SCOPES:
scope = BALROG_SERVER_SCOPES[alias]
break
else:
scope = BALROG_SERVER_SCOPES[
"default"]
if scope ==
"balrog:server:dep":
return "https://stage.balrog.nonprod.cloudops.mozgcp.net/api/v1"
return "https://aus5.mozilla.org/api/v1"
def find_localtest(fileUrls):
for channel
in fileUrls:
if "-localtest" in channel:
return channel
def populate_release_history(
product, branch, maxbuilds=4, maxsearch=10, partial_updates=
None
):
# Assuming we are using release branches when we know the list of previous
# releases in advance
if partial_updates
is not None:
return _populate_release_history(
product, branch, partial_updates=partial_updates
)
return _populate_nightly_history(
product, branch, maxbuilds=maxbuilds, maxsearch=maxsearch
)
def _populate_nightly_history(product, branch, maxbuilds=4, maxsearch=10):
"""Find relevant releases in Balrog
Not all releases have all platforms
and locales, due
to Taskcluster migration.
Args:
product (str): capitalized product name, AKA appName, e.g. Firefox
branch (str): branch name (mozilla-central)
maxbuilds (int): Maximum number of historical releases to populate
maxsearch(int): Traverse at most this many releases, to avoid
working through the entire history.
Returns:
json object based on data
from balrog api
results = {
'platform1': {
'locale1': {
'buildid1': mar_url,
'buildid2': mar_url,
'buildid3': mar_url,
},
'locale2': {
'target.partial-1.mar': {
'buildid1':
'mar_url'},
}
},
'platform2': {
}
}
"""
last_releases = get_sorted_releases(product, branch)
partial_mar_tmpl =
"target.partial-{}.mar"
builds = dict()
for release
in last_releases[:maxsearch]:
# maxbuilds in all categories, don't make any more queries
full = len(builds) > 0
and all(
len(builds[platform][locale]) >= maxbuilds
for platform
in builds
for locale
in builds[platform]
)
if full:
break
history = get_release_builds(release, branch)
for platform
in history[
"platforms"]:
if "alias" in history[
"platforms"][platform]:
continue
if platform
not in builds:
builds[platform] = dict()
for locale
in history[
"platforms"][platform][
"locales"]:
if locale
not in builds[platform]:
builds[platform][locale] = dict()
if len(builds[platform][locale]) >= maxbuilds:
continue
if "buildID" not in history[
"platforms"][platform][
"locales"][locale]:
continue
buildid = history[
"platforms"][platform][
"locales"][locale][
"buildID"]
if (
"completes" not in history[
"platforms"][platform][
"locales"][locale]
or len(
history[
"platforms"][platform][
"locales"][locale][
"completes"]
)
== 0
):
continue
url = history[
"platforms"][platform][
"locales"][locale][
"completes"][0][
"fileUrl"
]
nextkey = len(builds[platform][locale]) + 1
builds[platform][locale][partial_mar_tmpl.format(nextkey)] = {
"buildid": buildid,
"mar_url": url,
}
return builds
def _populate_release_history(product, branch, partial_updates):
builds = dict()
for version, release
in partial_updates.items():
prev_release_blob =
"{product}-{version}-build{build_number}".format(
product=product, version=version, build_number=release[
"buildNumber"]
)
partial_mar_key = f
"target-{version}.partial.mar"
history = get_release_builds(prev_release_blob, branch)
# use one of the localtest channels to avoid relying on bouncer
localtest = find_localtest(history[
"fileUrls"])
url_pattern = history[
"fileUrls"][localtest][
"completes"][
"*"]
for platform
in history[
"platforms"]:
if "alias" in history[
"platforms"][platform]:
continue
if platform
not in builds:
builds[platform] = dict()
for locale
in history[
"platforms"][platform][
"locales"]:
if locale
not in builds[platform]:
builds[platform][locale] = dict()
buildid = history[
"platforms"][platform][
"locales"][locale][
"buildID"]
url = url_pattern.replace(
"%OS_FTP%", FTP_PLATFORM_MAP[platform]
).replace(
"%LOCALE%", locale)
builds[platform][locale][partial_mar_key] = {
"buildid": buildid,
"mar_url": url,
"previousVersion": version,
"previousBuildNumber": release[
"buildNumber"],
"product": product,
}
return builds