Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(557)

Side by Side Diff: infra/tools/cros_pin/cros_pin.py

Issue 1403313002: Added `cros_pin` CrOS pin-bump tool. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Added pinfile.py tests, cleanup. Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « infra/tools/cros_pin/checkout.py ('k') | infra/tools/cros_pin/execute.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 import distutils.util
6 import logging
7 import os
8 import re
9 import tempfile
10
11 from infra.libs.gitiles import gitiles
12 from infra.tools.cros_pin import checkout, execute, pinfile
13 from infra.tools.cros_pin.logger import LOGGER
14
15 # Ths path of the Chromite repository.
16 CHROMITE_REPOSITORY = 'https://chromium.googlesource.com/chromiumos/chromite'
17
18 # The number of stable release branches to build in addition to the beta
19 # branch.
20 DEFAULT_STABLE_COUNT = 2
21
22 # Regular expression to match release branch names.
23 RELEASE_RE = re.compile(r'release-R(\d+)-.*')
24
25
26 def add_argparse_options(parser):
27 parser.add_argument('-d', '--dry-run',
28 action='store_true',
29 help="Stop short of submitting the CLs.")
30 parser.add_argument('-n', '--no-verify',
31 action='store_true',
32 help="Don't check that the specified pin exists.")
33 parser.add_argument('-C', '--checkout-path', metavar='PATH',
34 help="If specified, the checkout at PATH will be used instead of a "
35 "temporary one. If PATH does not exist, it will be created, and "
36 "the checkout will not be cleaned up. This is intended for "
37 "debugging.")
38 parser.add_argument('--chromite-repository', default=CHROMITE_REPOSITORY,
39 help="The Chromite repository to query (default is %(default)s).")
40 parser.add_argument('-b', '--bug',
41 help="Cite this BUG when creating CLs.")
42 parser.add_argument('-r', '--reviewer',
43 action='append', default=[],
44 help="Add this reviewer to the uploaded CL. If no reviewers are "
45 "specified, someone from the OWNERS file will be chosen.")
46 parser.add_argument('-m', '--commit-message',
47 help="Use this commit message instead of an auto-generated one.")
48 parser.add_argument('--no-commit', dest='commit', action='store_false',
49 help="Don't automatically mark generated CLs for commit queue.")
50
51 subparsers = parser.add_subparsers(help='CrOS Pin Subcommands')
52
53 # Subcommand: update
54 subp = subparsers.add_parser('update',
55 help=subcommand_update.__doc__)
56 subp.add_argument('-t', '--target',
57 choices=["existing", "external", "internal", "both"], default='existing',
58 help="Specifies which pin repositories to update. 'existing' (default) "
59 "updates all existing named pins. 'external', 'internal', and "
60 "'both' indicate that the pin should be updated in the external "
61 "and/or internal pin repositories, and should be added if not "
62 "currently present. Use these with caution!")
63 subp.add_argument('name',
64 help="The name of the pin to update.")
65 subp.add_argument('version', nargs='?',
66 help="The new commit hash for the pin. If empty, probe for tip-of-tree "
67 "of the branch sharing the pin's name.")
68 subp.set_defaults(func=subcommand_update)
69
70 # Subcommand: add-release
71 subp = subparsers.add_parser('add-release',
72 help=subcommand_add_release.__doc__)
73 subp.add_argument('--stable-count', metavar='COUNT',
74 type=int, default=DEFAULT_STABLE_COUNT,
75 help="Specifies the number of stable branches to preserve. (default is "
76 "%(default)s). The youngest COUNT release branch pins beyond the "
77 "newest will be preserved as stable branches, and any additional "
78 "release branches will be removed from the pins.")
79 subp.add_argument('branch',
80 help='The name of the release branch. Must begin with "release-R#".')
81 subp.add_argument('version', nargs='?',
82 help="The commit hash for the branch. If empty, use the branch's "
83 "tip-of-tree commit.")
84 subp.set_defaults(func=subcommand_add_release)
85
86
87 def checkout_for_args(args):
88 """A contextmanager that supplies the Checkout configured in args.
89
90 The Checkout's teardown() method will be invoked on cleanup.
91
92 Args:
93 args (argparse.Options): Parsed option list.
94 """
95 return checkout.Checkout.use(
96 path=args.checkout_path)
97
98 def pinfile_editor_from_args(args, c):
99 return pinfile.Editor(
100 c.path,
101 gitiles.Repository(args.chromite_repository),
102 validate=not args.no_verify)
103
104
105 def logging_verbosity():
106 count = 0
107 if LOGGER.level >= logging.INFO:
108 count += 1
109 if LOGGER.level >= logging.DEBUG:
110 count += 1
111 return ['-v'] * count
112
113
114 def get_release_version(v):
115 m = RELEASE_RE.match(v)
116 if not m:
117 return None
118 return int(m.group(1))
119
120
121 def subcommand_update(args):
122 """Update a single Chromite pin."""
123 create = (args.target != 'existing')
124 target_pins = []
125 if args.target in ('external', 'both', 'existing'):
126 target_pins.append(pinfile.EXTERNAL)
127 if args.target in ('internal', 'both', 'existing'):
128 target_pins.append(pinfile.INTERNAL)
129
130 with checkout_for_args(args) as c:
131 pfe = pinfile_editor_from_args(args, c)
132 tracker = UpdateTracker.from_args(args, c)
133
134 for pin in target_pins:
135 logging.debug('Updating target pin [%s]', pin)
136
137 # Update
138 pf = pfe.load(pin)
139 update = pf.update(args.name, version=args.version, create=create)
140 if not update:
141 LOGGER.debug('Did not update pins for [%s]', pin)
142 continue
143 tracker.add(pin, update)
144
145 LOGGER.debug('Updated pin set: %s', update)
146 if not tracker:
147 LOGGER.error('No pins were updated.')
148 return 1
149
150 # Regenerate slave pools for affected masters.
151 tracker.update()
152 for i in tracker.issues:
153 LOGGER.warning('Created Issue: %s', i)
154 return 0
155
156
157 def subcommand_add_release(args):
158 """Add a new release branch to the list of pins."""
159 with checkout_for_args(args) as c:
160 pfe = pinfile_editor_from_args(args, c)
161 tracker = UpdateTracker.from_args(args, c)
162
163 add_release = (get_release_version(args.branch), args.branch)
164 if add_release[0] is None:
165 raise ValueError("Invalid release branch: [%s]" % (args.branch,))
166
167 # Build a list of releases and their versions.
168 pf = pfe.load(pinfile.INTERNAL)
169 releases = [add_release]
170 for name, _ in pf.iterpins():
171 v = get_release_version(name)
172 if v == add_release[0]:
173 LOGGER.error('Release [%s] (%d) is already pinned.',
174 add_release[1], add_release[0])
175 return 1
176
177 if v is not None:
178 releases.append((v, name))
179 releases.sort(reverse=True)
180
181 # Shave off the top [stable_count+1] releases.
182 count = args.stable_count+1
183 releases, deleted = releases[:count], releases[count:]
184 if add_release not in releases:
185 raise ValueError("Updated releases do not include added (%s):\n%s" % (
186 add_release[1], '\n'.join(r[1] for r in releases)))
187
188 # Set the new releases.
189 tracker.add(pinfile.INTERNAL, pf.update(add_release[1], create=True))
190 for _, r in deleted:
191 tracker.add(pinfile.INTERNAL, pf.remove(r))
192
193 if not tracker:
194 LOGGER.error('No pins were updated.')
195 return 1
196
197 # Regenerate slave pools for affected masters.
198 tracker.update()
199 LOGGER.warning('Created issues:\n%s', '\n'.join(tracker.issues))
200 return 0
201
202
203 class SlavePoolUpdateError(Exception):
204 pass
205
206
207 class UpdateTracker(object):
208
209 RUNIT_PY = ('build', 'scripts', 'tools', 'runit.py')
210 SLAVE_ALLOC_UPDATE = ('build', 'scripts', 'tools', 'slave_alloc_update.py')
211
212 RE_ISSUE_CREATED = re.compile(r'^Issue created. URL: (.+)$')
213
214 def __init__(self, c, cq=False, bug=None, reviewers=None, dry_run=True):
215 self._c = c
216 self._cq = cq
217 self._bug = bug
218 self._reviewers = reviewers
219 self._dry_run = dry_run
220
221 self._updated = {}
222 self._issues = set()
223
224 @classmethod
225 def from_args(cls, args, c):
226 return cls(
227 c,
228 cq=args.commit,
229 bug=args.bug,
230 reviewers=args.reviewer,
231 dry_run=args.dry_run)
232
233 def __nonzero__(self):
234 return bool(self._updated)
235
236 @property
237 def issues(self):
238 return sorted(self._issues)
239
240 def add(self, pin, update):
241 self._updated.setdefault(pin, {})[update.name] = (update.fr, update.to)
242
243 def update(self):
244 LOGGER.info('Updating repositories: %s', self._updated)
245 affected_masters = set()
246 for pin in self._updated.iterkeys():
247 affected_masters.update(pin.masters)
248
249 failed_slave_pool_masters = []
250 for m in sorted(affected_masters):
251 try:
252 self._regenerate_slave_pool(m)
253 except SlavePoolUpdateError:
254 failed_slave_pool_masters.append(m)
255 if failed_slave_pool_masters:
256 LOGGER.error('Failed to update slave pools for %s. You may need to '
257 'add additional slaves the pool(s).',
258 failed_slave_pool_masters)
259 raise SlavePoolUpdateError("Failed to update slave pools.")
260
261 # Upload CLs for the affected repositories.
262 for pin, updates in self._updated.iteritems():
263 self._upload_patch(
264 self._c.subpath(*pin.base),
265 self._generate_commit_message(updates))
266
267 def _regenerate_slave_pool(self, master):
268 LOGGER.debug('Regenerating slave pool for: %s', master)
269 cmd = [
270 os.path.join(*self.RUNIT_PY),
271 os.path.join(*self.SLAVE_ALLOC_UPDATE),
272 ]
273 cmd += logging_verbosity()
274 cmd.append(master)
275
276 rv, stdout = execute.call(cmd, cwd=self._c.path)
277 if rv != 0:
278 LOGGER.exception('Failed to update slaves for master [%s] (%d):\n%s',
279 master, rv, stdout)
280 raise SlavePoolUpdateError()
281
282
283 def _upload_patch(self, repo_path, commit_msg):
284 # Check if the Git repository actually has changes.
285 diff_args = ['git', 'diff', '--no-ext-diff', '--exit-code']
286 if not LOGGER.isEnabledFor(logging.DEBUG):
287 diff_args.append('--quiet')
288 rv, diff = execute.call(diff_args, cwd=repo_path)
289 LOGGER.debug('Diff for [%s]:\n%s', repo_path, diff)
290 if rv == 0:
291 LOGGER.warning('No changes in repository; refusing to commit.')
292 return
293
294 LOGGER.debug('Creating commit in [%s] with message:\n%s',
295 repo_path, commit_msg)
296 execute.check_call(
297 ['git', 'checkout', '-b', '_cros_pin', '--track'],
298 cwd=repo_path)
299 execute.check_call(
300 ['git', 'commit', '--all', '--message', commit_msg],
301 cwd=repo_path)
302
303 LOGGER.debug('Uploading CL!')
304 args = [
305 'git', 'cl', 'upload',
306 '--bypass-hooks', # The CQ will take care of them!
307 '-t', commit_msg,
308 '-m', 'Auto-generated by `%s`' % (__name__,),
309 '-f',
310 ]
311 if self._cq:
312 print 'Commit? [Y/n]:',
313 input_string = raw_input()
314 if input_string != '' and not distutils.util.strtobool(input_string):
315 LOGGER.info('User opted not to commit; aborting.')
316 return
317 args.append('--use-commit-queue')
318 if not self._reviewers:
319 args.append('--tbr-owners')
320
321 output = execute.check_call(args, cwd=repo_path, dry_run=self._dry_run)
322 issue = None
323 for line in output.splitlines():
324 match = self.RE_ISSUE_CREATED.match(line)
325 if match:
326 issue = match.group(1)
327 LOGGER.debug('Extracted issue from output: %s', issue)
328 self._issues.add(issue)
329 break
330 else:
331 LOGGER.warning("Unable to extract issue from patch submission.")
332
333 def _generate_commit_message(self, updates):
334 lines = [
335 'CrOS: Update Chromite pin.',
336 '',
337 'Update ChromeOS Chromite pins.'
338 ]
339 for name, update in updates.iteritems():
340 if not update:
341 continue
342
343 fr, to = update
344 lines.append('- [%s]' % (name,))
345 if fr:
346 if to:
347 # Update from one commit to another.
348 lines.extend([
349 ' %s =>' % (fr,),
350 ' %s' % (to,),
351 ])
352 else:
353 # Added new pin.
354 lines.append(' - Deleted (was %s)' % (fr,))
355 elif to:
356 # Deleted a pin.
357 lines.append(' - Added => %s' % (to,))
358 lines.append('')
359
360 if self._bug:
361 lines.append('BUG=%s' % (self._bug,))
362 if self._reviewers:
363 lines.append('TBR=%s' % (', '.join(self._reviewers)))
364 return '\n'.join(lines)
OLDNEW
« no previous file with comments | « infra/tools/cros_pin/checkout.py ('k') | infra/tools/cros_pin/execute.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698