blob: fbb25593b69070a61a68667d5de2f0a42b8b858f [file] [log] [blame]
#!/usr/bin/env python
"""%prog [-d|--dryrun] [-u|--username `username`] [-b|--bypass-check]
[-l| --bypass-l10n-check] [-m|--bypass-mozconfig-check]
[-V| --version `version`] [-B --branch `branchname`]
[-N|--build-number `buildnumber`]
[-c| --release-config `releaseConfigFile`]
[-w| --whitelist `mozconfig_whitelist`]
[--l10n-dashboard-version version]
master:port
Wrapper script to sanity-check a release. Default behaviour is to check
the branch and revision specific in the release_configs, check if the
milestone and version# in the source repo match the
expected values in the release_configs, check the l10n repos & dashboard,
compare the nightly and release mozconfigs for a release branch against
a whitelist of known differences between the two. If all tests pass then
the master is reconfiged and then a senchange is generated to kick off
the release automation.
"""
try:
import simplejson as json
except ImportError:
import json
import logging
import site
import urllib2
from optparse import OptionParser
from os import path
from tempfile import mkdtemp
from shutil import rmtree
site.addsitedir(path.join(path.dirname(__file__), "../lib/python"))
from util.file import compare
from util.hg import make_hg_url, mercurial, update
from release.info import readReleaseConfig, getRepoMatchingBranch
from release.versions import getL10nDashboardVersion
from release.l10n import getShippedLocales
from release.platforms import getLocaleListFromShippedLocales
from release.sanity import check_buildbot, locale_diff, \
sendchange, verify_mozconfigs
from release.partials import Partial
from util.retry import retry
log = logging.getLogger(__name__)
error_tally = set()
HG = 'hg.mozilla.org'
def verify_repo(branch, revision, hghost):
"""Poll the hgweb interface for a given branch and revision to
make sure it exists"""
repo_url = make_hg_url(hghost, branch, revision=revision)
log.info("Checking for existence of %s..." % repo_url)
success = True
try:
repo_page = urllib2.urlopen(repo_url)
log.info("Got: %s !" % repo_page.geturl())
except urllib2.HTTPError:
log.error("Repo does not exist with required revision."
" Check again, or use -b to bypass")
success = False
error_tally.add('verify_repo')
return success
def verify_configs(configs_dir, revision, hghost, configs_repo, changesets,
filename):
"""Check the release_configs and l10n-changesets against tagged
revisions"""
release_config_file = path.join(configs_dir, 'mozilla', filename)
l10n_changesets_file = path.join(configs_dir, 'mozilla', changesets)
configs_url = make_hg_url(hghost, configs_repo, revision=revision,
filename=path.join('mozilla', filename))
l10n_url = make_hg_url(hghost, configs_repo, revision=revision,
filename=path.join('mozilla', changesets))
success = True
try:
official_configs = urllib2.urlopen(configs_url, timeout=10)
log.info("Comparing tagged revision %s to on-disk %s ..." % (
configs_url, filename))
if not compare(official_configs, release_config_file):
log.error("local configs do not match tagged revisions in repo")
success = False
error_tally.add('verify_configs')
l10n_changesets = urllib2.urlopen(l10n_url, timeout=10)
log.info("Comparing tagged revision %s to on-disk %s ..." % (
l10n_url, changesets))
if not compare(l10n_changesets, l10n_changesets_file):
log.error("local l10n-changesets do not match tagged revisions"
" in repo")
success = False
error_tally.add('verify_configs')
except (urllib2.HTTPError, urllib2.URLError):
log.error("cannot find configs in repo %s" % configs_url)
log.error("cannot find configs in repo %s" % l10n_url)
success = False
error_tally.add('verify_configs')
return success
def query_locale_revisions(l10n_changesets):
locales = {}
if l10n_changesets.endswith('.json'):
fh = open(l10n_changesets, 'r')
locales_json = json.load(fh)
fh.close()
for locale in locales_json:
locales[locale] = locales_json[locale]["revision"]
else:
for line in open(l10n_changesets, 'r'):
locale, revision = line.split()
locales[locale] = revision
return locales
def get_l10n_changesets(locale_url):
try:
urllib2.urlopen(locale_url, timeout=10)
return True
except urllib2.HTTPError, e:
reason = ""
if hasattr(e, 'reason'):
# Python 2.6 does not have reason
reason = e.reason
log.error("error checking l10n changeset %s: %d %s" % (locale_url, e.code, reason))
raise e
except urllib2.URLError:
log.error("timeout checking l10n changeset %s" % locale_url)
raise
def verify_l10n_changesets(hgHost, l10n_changesets):
"""Checks for the existance of all l10n changesets"""
success = True
locales = query_locale_revisions(l10n_changesets)
for locale in sorted(locales.keys()):
revision = locales[locale]
localePath = '%(repoPath)s/%(locale)s/file/%(revision)s' % {
'repoPath': releaseConfig['l10nRepoPath'].strip('/'),
'locale': locale,
'revision': revision,
}
locale_url = make_hg_url(hgHost, localePath, protocol='https')
log.info("Checking for existence l10n changeset %s %s in repo %s ..."
% (locale, revision, locale_url))
success = retry(get_l10n_changesets,
kwargs=dict(locale_url=locale_url), attempts=3,
sleeptime=1, retry_exceptions=(urllib2.HTTPError, urllib2.URLError))
if not success:
error_tally.add('verify_l10n')
return success
def verify_l10n_dashboard(l10n_changesets, l10n_dashboard_version=None):
"""Checks the l10n-changesets against the l10n dashboard"""
success = True
locales = query_locale_revisions(l10n_changesets)
if l10n_dashboard_version:
l10n_dashboard_version = getL10nDashboardVersion(
l10n_dashboard_version, releaseConfig['productName'],
parse_version=False)
else:
l10n_dashboard_version = getL10nDashboardVersion(
releaseConfig['version'], releaseConfig['productName'])
dash_url = 'https://l10n.mozilla.org/shipping/l10n-changesets?ms=%s' % \
l10n_dashboard_version
log.info("Comparing l10n changesets on dashboard %s to on-disk %s ..."
% (dash_url, l10n_changesets))
try:
dash_changesets = {}
for line in urllib2.urlopen(dash_url, timeout=10):
locale, revision = line.split()
dash_changesets[locale] = revision
for locale in locales:
revision = locales[locale]
dash_revision = dash_changesets.pop(locale, None)
if not dash_revision:
log.error("\tlocale %s missing on dashboard" % locale)
success = False
error_tally.add('verify_l10n_dashboard')
elif revision != dash_revision:
log.error("\tlocale %s revisions not matching: %s (config)"
" vs. %s (dashboard)"
% (locale, revision, dash_revision))
success = False
error_tally.add('verify_l10n_dashboard')
for locale in dash_changesets:
log.error("\tlocale %s missing in config" % locale)
success = False
error_tally.add('verify_l10n_dashboard')
except (urllib2.HTTPError, urllib2.URLError):
log.error("cannot find l10n dashboard at %s" % dash_url)
success = False
error_tally.add('verify_l10n_dashboard')
return success
def verify_l10n_shipped_locales(l10n_changesets, shipped_locales):
"""Ensure that our l10n-changesets on the master match the repo's shipped
locales list"""
success = True
locales = query_locale_revisions(l10n_changesets)
log.info("Comparing l10n changesets to shipped locales ...")
diff_list = locale_diff(locales, shipped_locales)
if len(diff_list) > 0:
log.error("l10n_changesets and shipped_locales differ on locales:"
" %s" % diff_list)
success = False
error_tally.add('verify_l10n_shipped_locales')
return success
def verify_options(cmd_options, config):
"""Check release_configs against command-line opts"""
success = True
if cmd_options.version and cmd_options.version != config['version']:
log.error("version passed in does not match release_configs")
success = False
error_tally.add('verify_options')
if cmd_options.buildNumber and \
int(cmd_options.buildNumber) != int(config['buildNumber']):
log.error("buildNumber passed in does not match release_configs")
success = False
error_tally.add('verify_options')
if not getRepoMatchingBranch(cmd_options.branch,
config['sourceRepositories']):
log.error("branch passed in does not exist in release config")
success = False
error_tally.add('verify_options')
if not cmd_options.skip_reconfig:
if not cmd_options.masters_json_file:
log.error("masters json file is required when not skipping reconfig")
success = False
error_tally.add('masters_json_file')
return success
def verify_partial(platforms, product, version, build_number,
HACK_first_released_versions=None, protocol='http',
server='ftp.mozilla.org'):
from distutils.version import LooseVersion
partial = Partial(product, version, build_number, protocol, server)
log.info("Checking for existence of %s complete mar file..." % partial)
complete_mar_name = partial.complete_mar_name()
for platform in platforms:
if HACK_first_released_versions and platform in HACK_first_released_versions:
if LooseVersion(version) < LooseVersion(HACK_first_released_versions[platform]):
# No partial for this!
continue
log.info("Platform: %s" % platform)
complete_mar_url = partial.complete_mar_url(platform=platform)
if partial.exists(platform=platform):
log.info("complete mar: %s exists, url: %s" % (complete_mar_name,
complete_mar_url))
else:
log.error("Requested file, %s, does not exist on %s"
" Check again, or use -b to bypass" % (complete_mar_name,
complete_mar_url))
error_tally.add('verify_partial')
return False
return True
if __name__ == '__main__':
parser = OptionParser(__doc__)
parser.set_defaults(
check=True,
checkL10n=True,
checkL10nDashboard=True,
checkMozconfigs=True,
dryrun=False,
username="cltbld",
loglevel=logging.INFO,
version=None,
buildNumber=None,
branch=None,
whitelist=path.abspath(path.join(path.dirname(__file__),
"mozconfig_whitelist")),
skip_reconfig=False,
configs_repo_url='build/buildbot-configs',
configs_branch='production',
concurrency=8,
skip_verify_configs=False,
checkMultiLocale=True,
)
parser.add_option(
"-b", "--bypass-check", dest="check", action="store_false",
help="don't bother verifying release repo's on this master")
parser.add_option(
"-l", "--bypass-l10n-check", dest="checkL10n", action="store_false",
help="don't bother verifying l10n milestones")
parser.add_option(
"--bypass-l10n-dashboard-check", dest="checkL10nDashboard",
action="store_false", help="don't verify l10n changesets against the dashboard (implied when --bypass-l10n-check is passed)")
parser.add_option(
"-m", "--bypass-mozconfig-check", dest="checkMozconfigs",
action="store_false", help="don't verify mozconfigs")
parser.add_option(
"-d", "--dryrun", "--dry-run", dest="dryrun", action="store_true",
help="just do the reconfig/checks, without starting anything")
parser.add_option(
"-u", "--username", dest="username",
help="specify a specific username to attach to the sendchange")
parser.add_option(
"-V", "--version", dest="version",
help="version string for release in format: x.x.x")
parser.add_option("-N", "--build-number", dest="buildNumber", type="int",
help="build number for this release, "
"uses release_config otherwise")
parser.add_option(
"-B", "--branch", dest="branch",
help="branch name for this release, uses release_config otherwise")
parser.add_option(
"-c", "--release-config", dest="releaseConfigFiles", action="append",
help="specify the release-config files (the first is primary)")
parser.add_option("-w", "--whitelist", dest="whitelist",
help="whitelist for known mozconfig differences")
parser.add_option(
"--l10n-dashboard-version", dest="l10n_dashboard_version",
help="Override L10N dashboard version")
parser.add_option("--skip-reconfig", dest="skip_reconfig",
action="store_true", help="Do not run reconfig")
parser.add_option("--configs-dir", dest="configs_dir",
help="buildbot-configs directory")
parser.add_option("--configs-repo-url", dest="configs_repo_url",
help="buildbot-configs repo URL")
parser.add_option("--configs-branch", dest="configs_branch",
help="buildbot-configs branch")
parser.add_option("--masters-json-file", dest="masters_json_file",
help="Path to production-masters.json file.")
parser.add_option('-j', dest='concurrency', type='int',
help='Fabric concurrency level')
parser.add_option("--skip-verify-configs", dest="skip_verify_configs",
action="store_true",
help="Do not verify configs agains remote repos")
parser.add_option("--bypass-multilocale-check", dest="checkMultiLocale",
action="store_false",
help="Do not verify that multilocale is enabled for Fennec")
options, args = parser.parse_args()
if not options.dryrun and not args:
parser.error("Need to provide a master to sendchange to,"
" or -d for a dryrun")
elif not options.branch:
parser.error("Need to provide a branch to release")
elif not options.releaseConfigFiles:
parser.error("Need to provide a release config file")
logging.basicConfig(level=options.loglevel,
format="%(asctime)s : %(levelname)s : %(message)s")
releaseConfig = None
test_success = True
buildNumber = options.buildNumber
products = []
check_buildbot()
if not options.dryrun and not options.skip_reconfig:
from util.fabric.common import check_fabric, FabricHelper
check_fabric()
if options.configs_dir:
configs_dir = options.configs_dir
cleanup_configs = False
else:
cleanup_configs = True
configs_dir = mkdtemp()
remote = make_hg_url(HG, options.configs_repo_url)
retry(mercurial, args=(remote, configs_dir),
kwargs={'branch': options.configs_branch})
update(configs_dir, options.configs_branch)
# https://bugzilla.mozilla.org/show_bug.cgi?id=678103#c5
# This goes through the list of config files in reverse order, which is a
# hacky way of making sure that the config file that's listed first is the
# one that's loaded in releaseConfig for the sendchange.
for releaseConfigFile in list(reversed(options.releaseConfigFiles)):
abs_release_config_file = path.join(configs_dir, 'mozilla',
releaseConfigFile)
releaseConfig = readReleaseConfig(abs_release_config_file)
products.append(releaseConfig['productName'])
if not options.buildNumber:
log.warn("No buildNumber specified, using buildNumber in"
" release_config, which may be out of date!")
options.buildNumber = releaseConfig['buildNumber']
if options.check:
site.addsitedir(path.join(configs_dir, 'mozilla'))
from config import BRANCHES
source_repo = 'mozilla'
try:
branchConfig = BRANCHES[options.branch]
except KeyError:
from thunderbird_config import BRANCHES
branchConfig = BRANCHES[options.branch]
source_repo = 'comm'
# Match command line options to defaults in release_configs
if not verify_options(options, releaseConfig):
test_success = False
log.error("Error verifying command-line options,"
" attempting checking repo")
# verify that mozconfigs for this release pass diff with nightly,
# compared to a whitelist
try:
repo_path = \
releaseConfig['sourceRepositories'][source_repo]['path']
revision = \
releaseConfig[
'sourceRepositories'][source_repo]['revision']
except KeyError:
try:
repo_path = \
releaseConfig['sourceRepositories']['mobile']['path']
revision = \
releaseConfig[
'sourceRepositories']['mobile']['revision']
except KeyError:
log.error("Can't determine sourceRepo for mozconfigs")
nightly_mozconfigs = {}
for p in releaseConfig['mozconfigs']:
nightly_mozconfigs[p] = branchConfig['platforms'][p]['src_mozconfig']
if options.checkMozconfigs and \
not verify_mozconfigs(
repo_path,
revision,
branchConfig['hghost'],
releaseConfig['productName'],
releaseConfig['mozconfigs'],
nightly_mozconfigs,
options.whitelist):
test_success = False
error_tally.add('verify_mozconfig')
log.error("Error verifying mozconfigs")
# verify that the release_configs on-disk match the tagged
# revisions in hg
l10nRevisionFile = path.join(configs_dir, 'mozilla',
releaseConfig['l10nRevisionFile'])
if not options.skip_verify_configs and \
not verify_configs(
configs_dir,
"%s_BUILD%s" % (releaseConfig['baseTag'], buildNumber),
branchConfig['hghost'],
options.configs_repo_url,
releaseConfig['l10nRevisionFile'],
releaseConfigFile):
test_success = False
log.error("Error verifying configs")
if options.checkL10n:
# verify that l10n changesets exist
if not verify_l10n_changesets(branchConfig['hghost'],
l10nRevisionFile):
test_success = False
log.error("Error verifying l10n changesets")
if options.checkL10nDashboard:
# verify that l10n changesets match the dashboard
if not verify_l10n_dashboard(
l10nRevisionFile,
options.l10n_dashboard_version):
test_success = False
log.error("Error verifying l10n dashboard changesets")
if options.checkMultiLocale:
if releaseConfig.get('enableMultiLocale'):
f = open(l10nRevisionFile)
if 'multilocale' not in f.read():
test_success = False
log.error("MultiLocale enabled but not present in l10n changesets")
f.close()
# verify that l10n changesets match the shipped locales
if releaseConfig.get('shippedLocalesPath'):
sr = releaseConfig['sourceRepositories'][source_repo]
sourceRepoPath = sr['path']
shippedLocales = getLocaleListFromShippedLocales(
getShippedLocales(
releaseConfig['productName'],
releaseConfig['appName'],
releaseConfig['version'],
releaseConfig['buildNumber'],
sourceRepoPath,
'https://hg.mozilla.org',
sr['revision'],
))
# l10n_changesets do not have an entry for en-US
if 'en-US' in shippedLocales:
shippedLocales.remove('en-US')
if not verify_l10n_shipped_locales(l10nRevisionFile,
shippedLocales):
test_success = False
log.error("Error verifying l10n_changesets matches"
" shipped_locales")
# verify that the relBranch + revision in the release_configs
# exists in hg
for sr in releaseConfig['sourceRepositories'].values():
sourceRepoPath = sr['path']
if not verify_repo(sourceRepoPath, sr['revision'],
branchConfig['hghost']):
test_success = False
log.error("Error verifying repos")
# check partial updates
partials = releaseConfig.get('partialUpdates')
if 'extraUpdates' in releaseConfig:
partials.extend(releaseConfig['extraUpdated'])
product = releaseConfig['productName']
platforms = releaseConfig['enUSPlatforms']
if partials:
for partial in partials:
build_number = partials[partial]['buildNumber']
# when bug 839926 lands, buildNumber must be None for releases
# but it might have a value for betas (beta *might* use
# unreleased builds see bug 1091694 c2)
if not verify_partial(platforms, product, partial,
build_number,
releaseConfig.get("HACK_first_released_version"),
server=releaseConfig['ftpServer']):
test_success = False
log.error("Error verifying partials")
if test_success:
if not options.dryrun:
if not options.skip_reconfig:
fabric_helper = FabricHelper(
masters_json_file=options.masters_json_file,
concurrency=options.concurrency,
roles=['build', 'scheduler'])
fabric_helper.update_and_reconfig()
sourceRepoPath = getRepoMatchingBranch(
options.branch, releaseConfig['sourceRepositories'])['path']
sendchange(
sourceRepoPath,
"%s_RELEASE" % releaseConfig['baseTag'],
options.username,
args[0],
products,
)
else:
log.info("Tests Passed! Did not run reconfig/sendchange."
" Rerun without `-d`")
if cleanup_configs:
log.info("Removing temporary directory: %s" % configs_dir)
rmtree(configs_dir)
else:
log.fatal("Tests Failed! Not running sendchange!")
log.fatal("Failed tests (run with -b to skip) :")
for error in error_tally:
log.fatal(error)
if cleanup_configs:
log.info("Not removing temporary directory: %s" % configs_dir)
exit(1)