| #!/usr/bin/python |
| |
| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import constants |
| import getpass |
| import json |
| import os |
| import shutil |
| import sys |
| import tempfile |
| import time |
| |
| if __name__ == '__main__': |
| sys.path.insert(0, constants.SOURCE_ROOT) |
| |
| from chromite.buildbot import repository |
| from chromite.buildbot import manifest_version |
| from chromite.lib import cros_build_lib |
| from chromite.lib import git |
| |
| |
| class ChromiteUpgradeNeeded(Exception): |
| """Exception thrown when it's detected that we need to upgrade chromite.""" |
| |
| def __init__(self, version=None): |
| Exception.__init__(self) |
| self.version = version |
| self.args = (version,) |
| |
| def __str__(self): |
| version_str = '' |
| if self.version: |
| version_str = " Need format version %r support." % (self.version,) |
| return ( |
| "Your version of cbuildbot is too old; please resync it, " |
| "and then retry your submission.%s" % (version_str,)) |
| |
| |
| class RemoteTryJob(object): |
| """Remote Tryjob that is submitted through a Git repo.""" |
| EXT_SSH_URL = os.path.join(constants.GERRIT_SSH_URL, |
| 'chromiumos/tryjobs') |
| INT_SSH_URL = os.path.join(constants.GERRIT_INT_SSH_URL, |
| 'chromeos/tryjobs') |
| |
| # In version 3, remote patches have an extra field. |
| # In version 4, cherry-picking is the norm, thus multiple patches are |
| # generated. |
| TRYJOB_FORMAT_VERSION = 4 |
| TRYSERVER_URL = 'http://chromegw/p/tryserver.chromiumos' |
| TRYJOB_FORMAT_FILE = '.tryjob_minimal_format_version' |
| |
| def __init__(self, options, bots, local_patches): |
| """Construct the object. |
| |
| Args: |
| options: The parsed options passed into cbuildbot. |
| bots: A list of configs to run tryjobs for. |
| local_patches: A list of LocalPatch objects. |
| """ |
| self.options = options |
| self.user = getpass.getuser() |
| cwd = os.path.dirname(os.path.realpath(__file__)) |
| self.user_email = git.GetProjectUserEmail(cwd) |
| cros_build_lib.Info('Using email:%s', self.user_email) |
| # Name of the job that appears on the waterfall. |
| patch_list = options.gerrit_patches + options.local_patches |
| self.name = options.remote_description |
| if self.name is None: |
| self.name = '' |
| if options.branch != 'master': |
| self.name = '[%s] ' % options.branch |
| self.name += ','.join(patch_list) |
| self.bots = bots[:] |
| self.slaves_request = options.slaves |
| self.description = ('name: %s\n patches: %s\nbots: %s' % |
| (self.name, patch_list, self.bots)) |
| self.extra_args = options.pass_through_args |
| if '--buildbot' not in self.extra_args: |
| self.extra_args.append('--remote-trybot') |
| |
| self.extra_args.append('--remote-version=%s' |
| % (self.TRYJOB_FORMAT_VERSION,)) |
| self.tryjob_repo = None |
| self.local_patches = local_patches |
| self.ssh_url = self.EXT_SSH_URL |
| self.manifest = None |
| if repository.IsARepoRoot(options.sourceroot): |
| self.manifest = git.ManifestCheckout.Cached(options.sourceroot) |
| if repository.IsInternalRepoCheckout(options.sourceroot): |
| self.ssh_url = self.INT_SSH_URL |
| |
| @property |
| def values(self): |
| return { |
| 'bot' : self.bots, |
| 'email' : [self.user_email], |
| 'extra_args' : self.extra_args, |
| 'name' : self.name, |
| 'slaves_request' : self.slaves_request, |
| 'user' : self.user, |
| 'version' : self.TRYJOB_FORMAT_VERSION, |
| } |
| |
| def _Submit(self, testjob, dryrun): |
| """Internal submission function. See Submit() for arg description.""" |
| # TODO(rcui): convert to shallow clone when that's available. |
| current_time = str(int(time.time())) |
| repository.CloneGitRepo(self.tryjob_repo, self.ssh_url) |
| version_path = os.path.join(self.tryjob_repo, |
| self.TRYJOB_FORMAT_FILE) |
| with open(version_path, 'r') as f: |
| try: |
| val = int(f.read().strip()) |
| except ValueError: |
| raise ChromiteUpgradeNeeded() |
| if val > self.TRYJOB_FORMAT_VERSION: |
| raise ChromiteUpgradeNeeded(val) |
| |
| ref_base = os.path.join('refs/tryjobs', self.user, current_time) |
| for patch in self.local_patches: |
| # Isolate the name; if it's a tag or a remote, let through. |
| # Else if it's a branch, get the full branch name minus refs/heads. |
| local_branch = git.StripRefsHeads(patch.ref, False) |
| ref_final = os.path.join(ref_base, local_branch, patch.sha1) |
| |
| self.manifest.AssertProjectIsPushable(patch.project) |
| data = self.manifest.projects[patch.project] |
| patch.Upload(data['push_url'], ref_final, dryrun=dryrun) |
| |
| # TODO(rcui): Pass in the remote instead of tag. http://crosbug.com/33937. |
| tag = constants.EXTERNAL_PATCH_TAG |
| if data['remote'] == constants.INTERNAL_REMOTE: |
| tag = constants.INTERNAL_PATCH_TAG |
| |
| self.extra_args.append('--remote-patches=%s:%s:%s:%s:%s' |
| % (patch.project, local_branch, ref_final, |
| patch.tracking_branch, tag)) |
| |
| push_branch = manifest_version.PUSH_BRANCH |
| remote_branch = ('origin', 'refs/remotes/origin/test') if testjob else None |
| git.CreatePushBranch(push_branch, self.tryjob_repo, sync=False, |
| remote_push_branch=remote_branch) |
| |
| file_name = '%s.%s' % (self.user, |
| current_time) |
| user_dir = os.path.join(self.tryjob_repo, self.user) |
| if not os.path.isdir(user_dir): |
| os.mkdir(user_dir) |
| |
| fullpath = os.path.join(user_dir, file_name) |
| with open(fullpath, 'w+') as job_desc_file: |
| json.dump(self.values, job_desc_file) |
| |
| cros_build_lib.RunCommand(['git', 'add', fullpath], cwd=self.tryjob_repo) |
| extra_env = { |
| # The committer field makes sure the creds match what the remote |
| # gerrit instance expects while the author field allows lookup |
| # on the console to work. http://crosbug.com/27939 |
| 'GIT_COMMITTER_EMAIL' : self.user_email, |
| 'GIT_AUTHOR_EMAIL' : self.user_email, |
| } |
| cros_build_lib.RunCommand(['git', 'commit', '-m', self.description], |
| cwd=self.tryjob_repo, extra_env=extra_env) |
| |
| try: |
| git.PushWithRetry( |
| push_branch, self.tryjob_repo, retries=3, dryrun=dryrun) |
| except cros_build_lib.RunCommandError: |
| cros_build_lib.Error( |
| 'Failed to submit tryjob. This could be due to too many ' |
| 'submission requests by users. Please try again.') |
| raise |
| |
| def Submit(self, workdir=None, testjob=False, dryrun=False): |
| """Submit the tryjob through Git. |
| |
| Args: |
| workdir: The directory to clone tryjob repo into. If you pass this |
| in, you are responsible for deleting the directory. Used for |
| testing. |
| testjob: Submit job to the test branch of the tryjob repo. The tryjob |
| will be ignored by production master. |
| dryrun: Setting to true will run everything except the final submit step. |
| """ |
| self.tryjob_repo = workdir |
| if self.tryjob_repo is None: |
| self.tryjob_repo = tempfile.mkdtemp() |
| |
| try: |
| self._Submit(testjob, dryrun) |
| finally: |
| if workdir is None: |
| shutil.rmtree(self.tryjob_repo) |
| |
| def GetTrybotConsoleLink(self): |
| """Get link to the console for the user.""" |
| return ('%s/console?name=%s' % (self.TRYSERVER_URL, self.user_email)) |
| |
| def GetTrybotWaterfallLink(self): |
| """Get link to the waterfall for the user.""" |
| # Note that this will only show the jobs submitted by the user in the last |
| # 24 hours. |
| return ('%s/waterfall?committer=%s' % (self.TRYSERVER_URL, self.user_email)) |