Upstream version 8.36.161.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / scripts / pushimage.py
1 # Copyright (c) 2013 The Chromium OS 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 """ChromeOS image pusher (from cbuildbot to signer).
6
7 This pushes files from the archive bucket to the signer bucket and marks
8 artifacts for signing (which a signing process will look for).
9 """
10
11 from __future__ import print_function
12
13 import ConfigParser
14 import cStringIO
15 import errno
16 import getpass
17 import os
18 import re
19 import tempfile
20 import textwrap
21
22 from chromite.buildbot import constants
23 from chromite.lib import commandline
24 from chromite.lib import cros_build_lib
25 from chromite.lib import git
26 from chromite.lib import gs
27 from chromite.lib import osutils
28 from chromite.lib import signing
29
30
31 # This will split a fully qualified ChromeOS version string up.
32 # R34-5126.0.0 will break into "34" and "5126.0.0".
33 VERSION_REGEX = r'^R([0-9]+)-([^-]+)'
34
35 # The test signers will scan this dir looking for test work.
36 # Keep it in sync with the signer config files [gs_test_buckets].
37 TEST_SIGN_BUCKET_BASE = 'gs://chromeos-throw-away-bucket/signer-tests'
38
39 # Ketsets that are only valid in the above test bucket.
40 TEST_KEYSETS = set(('test-keys-mp', 'test-keys-premp'))
41
42
43 class PushError(Exception):
44   """When an (unknown) error happened while trying to push artifacts."""
45
46
47 class MissingBoardInstructions(Exception):
48   """Raised when a board lacks any signer instructions."""
49
50
51 class InputInsns(object):
52   """Object to hold settings for a signable board.
53
54   Note: The format of the instruction file pushimage outputs (and the signer
55   reads) is not exactly the same as the instruction file pushimage reads.
56   """
57
58   def __init__(self, board):
59     self.board = board
60
61     config = ConfigParser.ConfigParser()
62     config.readfp(open(self.GetInsnFile('DEFAULT')))
63     try:
64       input_insn = self.GetInsnFile('recovery')
65       config.readfp(open(input_insn))
66     except IOError as e:
67       if e.errno == errno.ENOENT:
68         # This board doesn't have any signing instructions.
69         # This is normal for new or experimental boards.
70         raise MissingBoardInstructions(input_insn)
71       raise
72     self.cfg = config
73
74   def GetInsnFile(self, image_type):
75     """Find the signer instruction files for this board/image type.
76
77     Args:
78       image_type: The type of instructions to load.  It can be a common file
79         (like "DEFAULT"), or one of the --sign-types.
80
81     Returns:
82       Full path to the instruction file using |image_type| and |self.board|.
83     """
84     if image_type == image_type.upper():
85       name = image_type
86     elif image_type == 'recovery':
87       name = self.board
88     else:
89       name = '%s.%s' % (self.board, image_type)
90
91     return os.path.join(signing.INPUT_INSN_DIR, '%s.instructions' % name)
92
93   @staticmethod
94   def SplitCfgField(val):
95     """Split a string into multiple elements.
96
97     This centralizes our convention for multiple elements in the input files
98     being delimited by either a space or comma.
99
100     Args:
101       val: The string to split.
102
103     Returns:
104       The list of elements from having done split the string.
105     """
106     return val.replace(',', ' ').split()
107
108   def GetChannels(self):
109     """Return the list of channels to sign for this board.
110
111     If the board-specific config doesn't specify a preference, we'll use the
112     common settings.
113     """
114     return self.SplitCfgField(self.cfg.get('insns', 'channel'))
115
116   def GetKeysets(self):
117     """Return the list of keysets to sign for this board."""
118     return self.SplitCfgField(self.cfg.get('insns', 'keyset'))
119
120   def OutputInsns(self, image_type, output_file, sect_insns, sect_general):
121     """Generate the output instruction file for sending to the signer.
122
123     Note: The format of the instruction file pushimage outputs (and the signer
124     reads) is not exactly the same as the instruction file pushimage reads.
125
126     Args:
127       image_type: The type of image we will be signing (see --sign-types).
128       output_file: The file to write the new instruction file to.
129       sect_insns: Items to set/override in the [insns] section.
130       sect_general: Items to set/override in the [general] section.
131     """
132     config = ConfigParser.ConfigParser()
133     config.readfp(open(self.GetInsnFile(image_type)))
134
135     # Clear channel entry in instructions file, ensuring we only get
136     # one channel for the signer to look at.  Then provide all the
137     # other details for this signing request to avoid any ambiguity
138     # and to avoid relying on encoding data into filenames.
139     for sect, fields in zip(('insns', 'general'), (sect_insns, sect_general)):
140       if not config.has_section(sect):
141         config.add_section(sect)
142       for k, v in fields.iteritems():
143         config.set(sect, k, v)
144
145     output = cStringIO.StringIO()
146     config.write(output)
147     data = output.getvalue()
148     osutils.WriteFile(output_file, data)
149     cros_build_lib.Debug('generated insns file for %s:\n%s', image_type, data)
150
151
152 def MarkImageToBeSigned(ctx, tbs_base, insns_path, priority):
153   """Mark an instructions file for signing.
154
155   This will upload a file to the GS bucket flagging an image for signing by
156   the signers.
157
158   Args:
159     ctx: A viable gs.GSContext.
160     tbs_base: The full path to where the tobesigned directory lives.
161     insns_path: The path (relative to |tbs_base|) of the file to sign.
162     priority: Set the signing priority (lower == higher prio).
163
164   Returns:
165     The full path to the remote tobesigned file.
166   """
167   if priority < 0 or priority > 99:
168     raise ValueError('priority must be [0, 99] inclusive')
169
170   if insns_path.startswith(tbs_base):
171     insns_path = insns_path[len(tbs_base):].lstrip('/')
172
173   tbs_path = '%s/tobesigned/%02i,%s' % (tbs_base, priority,
174                                         insns_path.replace('/', ','))
175
176   with tempfile.NamedTemporaryFile(
177       bufsize=0, prefix='pushimage.tbs.') as temp_tbs_file:
178     lines = [
179         'PROG=%s' % __file__,
180         'USER=%s' % getpass.getuser(),
181         'HOSTNAME=%s' % cros_build_lib.GetHostName(fully_qualified=True),
182         'GIT_REV=%s' % git.RunGit(constants.CHROMITE_DIR,
183                                   ['rev-parse', 'HEAD']).output.rstrip(),
184     ]
185     osutils.WriteFile(temp_tbs_file.name, '\n'.join(lines) + '\n')
186     # The caller will catch gs.GSContextException for us.
187     ctx.Copy(temp_tbs_file.name, tbs_path)
188
189   return tbs_path
190
191
192 def PushImage(src_path, board, versionrev=None, profile=None, priority=50,
193               sign_types=None, dry_run=False, mock=False, force_keysets=()):
194   """Push the image from the archive bucket to the release bucket.
195
196   Args:
197     src_path: Where to copy the files from; can be a local path or gs:// URL.
198       Should be a full path to the artifacts in either case.
199     board: The board we're uploading artifacts for (e.g. $BOARD).
200     versionrev: The full Chromium OS version string (e.g. R34-5126.0.0).
201     profile: The board profile in use (e.g. "asan").
202     priority: Set the signing priority (lower == higher prio).
203     sign_types: If set, a set of types which we'll restrict ourselves to
204       signing.  See the --sign-types option for more details.
205     dry_run: Show what would be done, but do not upload anything.
206     mock: Upload to a testing bucket rather than the real one.
207     force_keysets: Set of keysets to use rather than what the inputs say.
208
209   Returns:
210     A dictionary that maps 'channel' -> ['gs://signer_instruction_uri1',
211                                          'gs://signer_instruction_uri2',
212                                          ...]
213   """
214   # Whether we hit an unknown error.  If so, we'll throw an error, but only
215   # at the end (so that we still upload as many files as possible).
216   unknown_error = False
217
218   if versionrev is None:
219     # Extract milestone/version from the directory name.
220     versionrev = os.path.basename(src_path)
221
222   # We only support the latest format here.  Older releases can use pushimage
223   # from the respective branch which deals with legacy cruft.
224   m = re.match(VERSION_REGEX, versionrev)
225   if not m:
226     raise ValueError('version %s does not match %s' %
227                      (versionrev, VERSION_REGEX))
228   milestone = m.group(1)
229   version = m.group(2)
230
231   # Normalize board to always use dashes not underscores.  This is mostly a
232   # historical artifact at this point, but we can't really break it since the
233   # value is used in URLs.
234   boardpath = board.replace('_', '-')
235   if profile is not None:
236     boardpath += '-%s' % profile.replace('_', '-')
237
238   ctx = gs.GSContext(dry_run=dry_run)
239
240   try:
241     input_insns = InputInsns(board)
242   except MissingBoardInstructions as e:
243     cros_build_lib.Warning('board "%s" is missing base instruction file: %s',
244                            board, e)
245     cros_build_lib.Warning('not uploading anything for signing')
246     return
247   channels = input_insns.GetChannels()
248
249   # We want force_keysets as a set, and keysets as a list.
250   force_keysets = set(force_keysets)
251   keysets = list(force_keysets) if force_keysets else input_insns.GetKeysets()
252
253   if mock:
254     cros_build_lib.Info('Upload mode: mock; signers will not process anything')
255     tbs_base = gs_base = os.path.join(constants.TRASH_BUCKET, 'pushimage-tests',
256                                       getpass.getuser())
257   elif TEST_KEYSETS & force_keysets:
258     cros_build_lib.Info('Upload mode: test; signers will process test keys')
259     # We need the tbs_base to be in the place the signer will actually scan.
260     tbs_base = TEST_SIGN_BUCKET_BASE
261     gs_base = os.path.join(tbs_base, getpass.getuser())
262   else:
263     cros_build_lib.Info('Upload mode: normal; signers will process the images')
264     tbs_base = gs_base = constants.RELEASE_BUCKET
265
266   sect_general = {
267       'config_board': board,
268       'board': boardpath,
269       'version': version,
270       'versionrev': versionrev,
271       'milestone': milestone,
272   }
273   sect_insns = {}
274
275   if dry_run:
276     cros_build_lib.Info('DRY RUN MODE ACTIVE: NOTHING WILL BE UPLOADED')
277   cros_build_lib.Info('Signing for channels: %s', ' '.join(channels))
278   cros_build_lib.Info('Signing for keysets : %s', ' '.join(keysets))
279
280   instruction_urls = {}
281
282   def _ImageNameBase(image_type=None):
283     lmid = ('%s-' % image_type) if image_type else ''
284     return 'ChromeOS-%s%s-%s' % (lmid, versionrev, boardpath)
285
286   for channel in channels:
287     cros_build_lib.Debug('\n\n#### CHANNEL: %s ####\n', channel)
288     sect_insns['channel'] = channel
289     sub_path = '%s-channel/%s/%s' % (channel, boardpath, version)
290     dst_path = '%s/%s' % (gs_base, sub_path)
291     cros_build_lib.Info('Copying images to %s', dst_path)
292
293     recovery_base = _ImageNameBase('recovery')
294     factory_base = _ImageNameBase('factory')
295     firmware_base = _ImageNameBase('firmware')
296     test_base = _ImageNameBase('test')
297     hwqual_tarball = 'chromeos-hwqual-%s-%s.tar.bz2' % (board, versionrev)
298
299     # Upload all the files first before flagging them for signing.
300     files_to_copy = (
301         # <src>                          <dst>
302         # <signing type>                 <sfx>
303         ('recovery_image.tar.xz',        recovery_base,          'tar.xz',
304          'recovery'),
305
306         ('factory_image.zip',            factory_base,           'zip',
307          'factory'),
308
309         ('firmware_from_source.tar.bz2', firmware_base,          'tar.bz2',
310          'firmware'),
311
312         ('image.zip',                    _ImageNameBase(),       'zip', ''),
313         ('chromiumos_test_image.tar.xz', test_base,              'tar.xz', ''),
314         ('debug.tgz',                    'debug-%s' % boardpath, 'tgz', ''),
315         (hwqual_tarball,                 '', '', ''),
316         ('au-generator.zip',             '', '', ''),
317     )
318     files_to_sign = []
319     for src, dst, sfx, image_type in files_to_copy:
320       if not dst:
321         dst = src
322       elif sfx:
323         dst += '.%s' % sfx
324       try:
325         ctx.Copy(os.path.join(src_path, src), os.path.join(dst_path, dst))
326       except gs.GSNoSuchKey:
327         cros_build_lib.Warning('Skipping %s as it does not exist', src)
328         continue
329       except gs.GSContextException:
330         unknown_error = True
331         cros_build_lib.Error('Skipping %s due to unknown GS error', src,
332                              exc_info=True)
333         continue
334
335       if image_type:
336         dst_base = dst[:-(len(sfx) + 1)]
337         assert dst == '%s.%s' % (dst_base, sfx)
338         files_to_sign += [[image_type, dst_base, '.%s' % sfx]]
339
340     # Now go through the subset for signing.
341     for keyset in keysets:
342       cros_build_lib.Debug('\n\n#### KEYSET: %s ####\n', keyset)
343       sect_insns['keyset'] = keyset
344       for image_type, dst_name, suffix in files_to_sign:
345         dst_archive = '%s%s' % (dst_name, suffix)
346         sect_general['archive'] = dst_archive
347         sect_general['type'] = image_type
348
349         # See if the caller has requested we only sign certain types.
350         if sign_types:
351           if not image_type in sign_types:
352             cros_build_lib.Info('Skipping %s signing as it was not requested',
353                                 image_type)
354             continue
355         else:
356           # In the default/automatic mode, only flag files for signing if the
357           # archives were actually uploaded in a previous stage.
358           gs_artifact_path = os.path.join(dst_path, dst_archive)
359           try:
360             exists = ctx.Exists(gs_artifact_path)
361           except gs.GSContextException:
362             unknown_error = True
363             exists = False
364             cros_build_lib.Error('Unknown error while checking %s',
365                                  gs_artifact_path, exc_info=True)
366           if not exists:
367             cros_build_lib.Info('%s does not exist.  Nothing to sign.',
368                                 gs_artifact_path)
369             continue
370
371         input_insn_path = input_insns.GetInsnFile(image_type)
372         if not os.path.exists(input_insn_path):
373           cros_build_lib.Info('%s does not exist.  Nothing to sign.',
374                               input_insn_path)
375           continue
376
377         # Generate the insn file for this artifact that the signer will use,
378         # and flag it for signing.
379         with tempfile.NamedTemporaryFile(
380             bufsize=0, prefix='pushimage.insns.') as insns_path:
381           input_insns.OutputInsns(image_type, insns_path.name, sect_insns,
382                                   sect_general)
383
384           gs_insns_path = '%s/%s' % (dst_path, dst_name)
385           if keyset != keysets[0]:
386             gs_insns_path += '-%s' % keyset
387           gs_insns_path += '.instructions'
388
389           try:
390             ctx.Copy(insns_path.name, gs_insns_path)
391           except gs.GSContextException:
392             unknown_error = True
393             cros_build_lib.Error('Unknown error while uploading insns %s',
394                                  gs_insns_path, exc_info=True)
395             continue
396
397           try:
398             MarkImageToBeSigned(ctx, tbs_base, gs_insns_path, priority)
399           except gs.GSContextException:
400             unknown_error = True
401             cros_build_lib.Error('Unknown error while marking for signing %s',
402                                  gs_insns_path, exc_info=True)
403             continue
404           cros_build_lib.Info('Signing %s image %s', image_type, gs_insns_path)
405           instruction_urls.setdefault(channel, []).append(gs_insns_path)
406
407   if unknown_error:
408     raise PushError('hit some unknown error(s)', instruction_urls)
409
410   return instruction_urls
411
412
413 def main(argv):
414   parser = commandline.ArgumentParser(description=__doc__)
415
416   # The type of image_dir will strip off trailing slashes (makes later
417   # processing simpler and the display prettier).
418   parser.add_argument('image_dir', default=None, type='local_or_gs_path',
419                       help='full path of source artifacts to upload')
420   parser.add_argument('--board', default=None, required=True,
421                       help='board to generate symbols for')
422   parser.add_argument('--profile', default=None,
423                       help='board profile in use (e.g. "asan")')
424   parser.add_argument('--version', default=None,
425                       help='version info (normally extracted from image_dir)')
426   parser.add_argument('-n', '--dry-run', default=False, action='store_true',
427                       help='show what would be done, but do not upload')
428   parser.add_argument('-M', '--mock', default=False, action='store_true',
429                       help='upload things to a testing bucket (dev testing)')
430   parser.add_argument('--test-sign-mp', default=False, action='store_true',
431                       help='mung signing behavior to sign w/test mp keys')
432   parser.add_argument('--test-sign-premp', default=False, action='store_true',
433                       help='mung signing behavior to sign w/test premp keys')
434   parser.add_argument('--priority', type=int, default=50,
435                       help='set signing priority (lower == higher prio)')
436   parser.add_argument('--sign-types', default=None, nargs='+',
437                       choices=('recovery', 'factory', 'firmware'),
438                       help='only sign specified image types')
439   parser.add_argument('--yes', action='store_true', default=False,
440                       help='answer yes to all prompts')
441
442   opts = parser.parse_args(argv)
443   opts.Freeze()
444
445   force_keysets = set()
446   if opts.test_sign_mp:
447     force_keysets.add('test-keys-mp')
448   if opts.test_sign_premp:
449     force_keysets.add('test-keys-premp')
450
451   # If we aren't using mock or test or dry run mode, then let's prompt the user
452   # to make sure they actually want to do this.  It's rare that people want to
453   # run this directly and hit the release bucket.
454   if not (opts.mock or force_keysets or opts.dry_run) and not opts.yes:
455     prolog = '\n'.join(textwrap.wrap(textwrap.dedent(
456         'Uploading images for signing to the *release* bucket is not something '
457         'you generally should be doing yourself.'), 80)).strip()
458     if not cros_build_lib.BooleanPrompt(
459         prompt='Are you sure you want to sign these images',
460         default=False, prolog=prolog):
461       cros_build_lib.Die('better safe than sorry')
462
463   PushImage(opts.image_dir, opts.board, versionrev=opts.version,
464             profile=opts.profile, priority=opts.priority,
465             sign_types=opts.sign_types, dry_run=opts.dry_run, mock=opts.mock,
466             force_keysets=force_keysets)