Upstream version 8.36.161.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / buildbot / remote_try.py
1 #!/usr/bin/python
2
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.
6
7 """Code related to Remote tryjobs."""
8
9 import constants
10 import getpass
11 import json
12 import os
13 import sys
14 import time
15
16 if __name__ == '__main__':
17   sys.path.insert(0, constants.SOURCE_ROOT)
18
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
24
25
26 class ChromiteUpgradeNeeded(Exception):
27   """Exception thrown when it's detected that we need to upgrade chromite."""
28
29   def __init__(self, version=None):
30     Exception.__init__(self)
31     self.version = version
32     self.args = (version,)
33
34   def __str__(self):
35     version_str = ''
36     if self.version:
37       version_str = "  Need format version %r support." % (self.version,)
38     return (
39         "Your version of cbuildbot is too old; please resync it, "
40         "and then retry your submission.%s" % (version_str,))
41
42
43 class ValidationError(Exception):
44   """Thrown when tryjob validation fails."""
45
46
47 class RemoteTryJob(object):
48   """Remote Tryjob that is submitted through a Git repo."""
49   EXTERNAL_URL = os.path.join(constants.EXTERNAL_GOB_URL,
50                             'chromiumos/tryjobs')
51   INTERNAL_URL = os.path.join(constants.INTERNAL_GOB_URL,
52                               'chromeos/tryjobs')
53
54   # In version 3, remote patches have an extra field.
55   # In version 4, cherry-picking is the norm, thus multiple patches are
56   # generated.
57   TRYJOB_FORMAT_VERSION = 4
58   TRYJOB_FORMAT_FILE = '.tryjob_minimal_format_version'
59
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
67   #   the description.
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
76   PADDING = 50
77
78   def __init__(self, options, bots, local_patches):
79     """Construct the object.
80
81     Args:
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.
85     """
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
95     if self.name is None:
96       self.name = ''
97       if options.branch != 'master':
98         self.name = '[%s] ' % options.branch
99
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,)
104
105     self.bots = bots[:]
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')
112
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',)
118     self.manifest = None
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',)
124
125   @property
126   def values(self):
127     return {
128         'bot' : self.bots,
129         'email' : [self.user_email],
130         'extra_args' : self.extra_args,
131         'name' : self.name,
132         'slaves_request' : self.slaves_request,
133         'user' : self.user,
134         'version' : self.TRYJOB_FORMAT_VERSION,
135         }
136
137   def _VerifyForBuildbot(self):
138     """Early validation, to ensure the job can be processed by buildbot."""
139
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] + '...'
147
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 '
155           '-g flag instead.')
156
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()))
161
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)
168
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)
173
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
178
179       self.extra_args.append('--remote-patches=%s:%s:%s:%s:%s'
180                              % (patch.project, local_branch, ref_final,
181                                 patch.tracking_branch, tag))
182
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:
188       try:
189         val = int(f.read().strip())
190       except ValueError:
191         raise ChromiteUpgradeNeeded()
192       if val > self.TRYJOB_FORMAT_VERSION:
193         raise ChromiteUpgradeNeeded(val)
194     push_branch = manifest_version.PUSH_BRANCH
195
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)
199
200     file_name = '%s.%s' % (self.user,
201                            current_time)
202     user_dir = os.path.join(workdir, self.user)
203     if not os.path.isdir(user_dir):
204       os.mkdir(user_dir)
205
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)
209
210     git.RunGit(workdir, ['add', fullpath])
211     extra_env = {
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,
217     }
218     git.RunGit(workdir, ['commit', '-m', self.description],
219                extra_env=extra_env)
220
221     try:
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.')
227       raise
228
229   def Submit(self, workdir=None, testjob=False, dryrun=False):
230     """Submit the tryjob through Git.
231
232     Args:
233       workdir: The directory to clone tryjob repo into.  If you pass this
234                in, you are responsible for deleting the directory.  Used for
235                testing.
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.
239     """
240     if workdir is None:
241       with self.repo_cache.Lookup(self.cache_key) as ref:
242         self._Submit(ref.path, testjob, dryrun)
243     else:
244       self._Submit(workdir, testjob, dryrun)
245
246   def GetTrybotConsoleLink(self):
247     """Get link to the console for the user."""
248     return ('%s/console?name=%s' % (constants.TRYBOT_DASHBOARD,
249                                     self.user_email))
250
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
254     # 24 hours.
255     return ('%s/waterfall?committer=%s' % (constants.TRYBOT_DASHBOARD,
256                                            self.user_email))