3 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
7 """Code related to Remote tryjobs."""
16 if __name__ == '__main__':
17 sys.path.insert(0, constants.SOURCE_ROOT)
19 from chromite.buildbot import repository
20 from chromite.buildbot import manifest_version
21 from chromite.lib import cros_build_lib
22 from chromite.lib import cache
23 from chromite.lib import git
26 class ChromiteUpgradeNeeded(Exception):
27 """Exception thrown when it's detected that we need to upgrade chromite."""
29 def __init__(self, version=None):
30 Exception.__init__(self)
31 self.version = version
32 self.args = (version,)
37 version_str = " Need format version %r support." % (self.version,)
39 "Your version of cbuildbot is too old; please resync it, "
40 "and then retry your submission.%s" % (version_str,))
43 class ValidationError(Exception):
44 """Thrown when tryjob validation fails."""
47 class RemoteTryJob(object):
48 """Remote Tryjob that is submitted through a Git repo."""
49 EXTERNAL_URL = os.path.join(constants.EXTERNAL_GOB_URL,
51 INTERNAL_URL = os.path.join(constants.INTERNAL_GOB_URL,
54 # In version 3, remote patches have an extra field.
55 # In version 4, cherry-picking is the norm, thus multiple patches are
57 TRYJOB_FORMAT_VERSION = 4
58 TRYJOB_FORMAT_FILE = '.tryjob_minimal_format_version'
60 # Constants for controlling the length of JSON fields sent to buildbot.
61 # - The trybot description is shown when the run starts, and helps users
62 # distinguish between their various runs. If no trybot description is
63 # specified, the list of patches is used as the description. The buildbot
64 # database limits this field to MAX_DESCRIPTION_LENGTH characters.
65 # - When checking the trybot description length, we also add some PADDING
66 # to give buildbot room to add extra formatting around the fields used in
68 # - We limit the number of patches listed in the description to
69 # MAX_PATCHES_IN_DESCRIPTION. This is for readability only.
70 # - Every individual field that is stored in a buildset is limited to
71 # MAX_PROPERTY_LENGTH. We use this to ensure that our serialized list of
72 # arguments fits within that limit.
73 MAX_DESCRIPTION_LENGTH = 256
74 MAX_PATCHES_IN_DESCRIPTION = 10
75 MAX_PROPERTY_LENGTH = 1023
78 def __init__(self, options, bots, local_patches):
79 """Construct the object.
82 options: The parsed options passed into cbuildbot.
83 bots: A list of configs to run tryjobs for.
84 local_patches: A list of LocalPatch objects.
86 self.options = options
87 self.user = getpass.getuser()
88 self.repo_cache = cache.DiskCache(self.options.cache_dir)
89 cwd = os.path.dirname(os.path.realpath(__file__))
90 self.user_email = git.GetProjectUserEmail(cwd)
91 cros_build_lib.Info('Using email:%s', self.user_email)
92 # Name of the job that appears on the waterfall.
93 patch_list = options.gerrit_patches + options.local_patches
94 self.name = options.remote_description
97 if options.branch != 'master':
98 self.name = '[%s] ' % options.branch
100 self.name += ','.join(patch_list[:self.MAX_PATCHES_IN_DESCRIPTION])
101 if len(patch_list) > self.MAX_PATCHES_IN_DESCRIPTION:
102 remaining_patches = len(patch_list) - self.MAX_PATCHES_IN_DESCRIPTION
103 self.name += '... (%d more CLs)' % (remaining_patches,)
106 self.slaves_request = options.slaves
107 self.description = ('name: %s\n patches: %s\nbots: %s' %
108 (self.name, patch_list, self.bots))
109 self.extra_args = options.pass_through_args
110 if '--buildbot' not in self.extra_args:
111 self.extra_args.append('--remote-trybot')
113 self.extra_args.append('--remote-version=%s'
114 % (self.TRYJOB_FORMAT_VERSION,))
115 self.local_patches = local_patches
116 self.repo_url = self.EXTERNAL_URL
117 self.cache_key = ('trybot',)
119 if repository.IsARepoRoot(options.sourceroot):
120 self.manifest = git.ManifestCheckout.Cached(options.sourceroot)
121 if repository.IsInternalRepoCheckout(options.sourceroot):
122 self.repo_url = self.INTERNAL_URL
123 self.cache_key = ('trybot-internal',)
129 'email' : [self.user_email],
130 'extra_args' : self.extra_args,
132 'slaves_request' : self.slaves_request,
134 'version' : self.TRYJOB_FORMAT_VERSION,
137 def _VerifyForBuildbot(self):
138 """Early validation, to ensure the job can be processed by buildbot."""
140 # Buildbot stores the trybot description in a property with a 256
141 # character limit. Validate that our description is well under the limit.
142 if (len(self.user) + len(self.name) + self.PADDING >
143 self.MAX_DESCRIPTION_LENGTH):
144 cros_build_lib.Warning(
145 'remote tryjob description is too long, truncating it')
146 self.name = self.name[:self.MAX_DESCRIPTION_LENGTH - self.PADDING] + '...'
148 # Buildbot will set extra_args as a buildset 'property'. It will store
149 # the property in its database in JSON form. The limit of the database
150 # field is 1023 characters.
151 if len(json.dumps(self.extra_args)) > self.MAX_PROPERTY_LENGTH:
152 raise ValidationError(
153 'The number of extra arguments passed to cbuildbot has exceeded the '
154 'limit. If you have a lot of local patches, upload them and use the '
157 def _Submit(self, workdir, testjob, dryrun):
158 """Internal submission function. See Submit() for arg description."""
159 # TODO(rcui): convert to shallow clone when that's available.
160 current_time = str(int(time.time()))
162 ref_base = os.path.join('refs/tryjobs', self.user_email, current_time)
163 for patch in self.local_patches:
164 # Isolate the name; if it's a tag or a remote, let through.
165 # Else if it's a branch, get the full branch name minus refs/heads.
166 local_branch = git.StripRefsHeads(patch.ref, False)
167 ref_final = os.path.join(ref_base, local_branch, patch.sha1)
169 checkout = patch.GetCheckout(self.manifest)
170 checkout.AssertPushable()
171 print 'Uploading patch %s' % patch
172 patch.Upload(checkout['push_url'], ref_final, dryrun=dryrun)
174 # TODO(rcui): Pass in the remote instead of tag. http://crosbug.com/33937.
175 tag = constants.EXTERNAL_PATCH_TAG
176 if checkout['remote'] == constants.INTERNAL_REMOTE:
177 tag = constants.INTERNAL_PATCH_TAG
179 self.extra_args.append('--remote-patches=%s:%s:%s:%s:%s'
180 % (patch.project, local_branch, ref_final,
181 patch.tracking_branch, tag))
183 self._VerifyForBuildbot()
184 repository.UpdateGitRepo(workdir, self.repo_url)
185 version_path = os.path.join(workdir,
186 self.TRYJOB_FORMAT_FILE)
187 with open(version_path, 'r') as f:
189 val = int(f.read().strip())
191 raise ChromiteUpgradeNeeded()
192 if val > self.TRYJOB_FORMAT_VERSION:
193 raise ChromiteUpgradeNeeded(val)
194 push_branch = manifest_version.PUSH_BRANCH
196 remote_branch = ('origin', 'refs/remotes/origin/test') if testjob else None
197 git.CreatePushBranch(push_branch, workdir, sync=False,
198 remote_push_branch=remote_branch)
200 file_name = '%s.%s' % (self.user,
202 user_dir = os.path.join(workdir, self.user)
203 if not os.path.isdir(user_dir):
206 fullpath = os.path.join(user_dir, file_name)
207 with open(fullpath, 'w+') as job_desc_file:
208 json.dump(self.values, job_desc_file)
210 git.RunGit(workdir, ['add', fullpath])
212 # The committer field makes sure the creds match what the remote
213 # gerrit instance expects while the author field allows lookup
214 # on the console to work. http://crosbug.com/27939
215 'GIT_COMMITTER_EMAIL' : self.user_email,
216 'GIT_AUTHOR_EMAIL' : self.user_email,
218 git.RunGit(workdir, ['commit', '-m', self.description],
222 git.PushWithRetry(push_branch, workdir, retries=3, dryrun=dryrun)
223 except cros_build_lib.RunCommandError:
224 cros_build_lib.Error(
225 'Failed to submit tryjob. This could be due to too many '
226 'submission requests by users. Please try again.')
229 def Submit(self, workdir=None, testjob=False, dryrun=False):
230 """Submit the tryjob through Git.
233 workdir: The directory to clone tryjob repo into. If you pass this
234 in, you are responsible for deleting the directory. Used for
236 testjob: Submit job to the test branch of the tryjob repo. The tryjob
237 will be ignored by production master.
238 dryrun: Setting to true will run everything except the final submit step.
241 with self.repo_cache.Lookup(self.cache_key) as ref:
242 self._Submit(ref.path, testjob, dryrun)
244 self._Submit(workdir, testjob, dryrun)
246 def GetTrybotConsoleLink(self):
247 """Get link to the console for the user."""
248 return ('%s/console?name=%s' % (constants.TRYBOT_DASHBOARD,
251 def GetTrybotWaterfallLink(self):
252 """Get link to the waterfall for the user."""
253 # Note that this will only show the jobs submitted by the user in the last
255 return ('%s/waterfall?committer=%s' % (constants.TRYBOT_DASHBOARD,