blob: b3c66003dbcfd0c79a52e9cd8ab061435ef86bf6 [file] [log] [blame]
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08001# -*- coding: utf-8 -*-
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08002# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""ChromeOS utility.
6
7Terminology used in this module.
8 short_version: ChromeOS version number without milestone, like "9876.0.0".
9 full_version: ChromeOS version number with milestone, like "R62-9876.0.0".
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080010 snapshot_version: ChromeOS version number with milestone and snapshot id,
11 like "R62-9876.0.0-12345".
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080012 version: if not specified, it could be in short or full format.
13"""
14
15from __future__ import print_function
Kuang-che Wub9705bd2018-06-28 17:59:18 +080016import ast
Kuang-che Wu72b5a572019-10-29 20:37:57 +080017import calendar
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080018import datetime
Kuang-che Wu721e8902021-03-19 12:18:53 +080019import enum
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080020import errno
Kuang-che Wu9d14c162020-11-03 19:35:18 +080021import glob
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080022import json
23import logging
24import os
25import re
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +080026import shutil
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080027import subprocess
Kuang-che Wud558a042020-06-06 02:11:00 +080028import sys
Kuang-che Wu721e8902021-03-19 12:18:53 +080029import tempfile
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080030import time
Kuang-che Wu721e8902021-03-19 12:18:53 +080031import urllib.parse
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080032
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +080033from google.protobuf import json_format
34
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +080035from bisect_kit import buildbucket_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080036from bisect_kit import cli
Kuang-che Wue4bae0b2018-07-19 12:10:14 +080037from bisect_kit import codechange
Kuang-che Wu721e8902021-03-19 12:18:53 +080038from bisect_kit import common
Kuang-che Wu3eb6b502018-06-06 16:15:18 +080039from bisect_kit import cr_util
Kuang-che Wue121fae2018-11-09 16:18:39 +080040from bisect_kit import errors
Kuang-che Wubfc4a642018-04-19 11:54:08 +080041from bisect_kit import git_util
Kuang-che Wufb553102018-10-02 18:14:29 +080042from bisect_kit import locking
Kuang-che Wubfc4a642018-04-19 11:54:08 +080043from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080044from bisect_kit import util
45
46logger = logging.getLogger(__name__)
47
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080048re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
Kuang-che Wuacb6efd2018-04-25 18:52:58 +080049re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
Zheng-Jie Chang127c3302019-09-10 17:17:04 +080050re_chromeos_snapshot_version = r'^R\d+-\d+\.\d+\.\d+-\d+$'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080051
Kuang-che Wu721e8902021-03-19 12:18:53 +080052gs_archive_base = 'gs://chromeos-image-archive/'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080053gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080054
55# Assume gsutil is in PATH.
56gsutil_bin = 'gsutil'
Zheng-Jie Changb8697042019-10-29 16:03:26 +080057
58# Since snapshots with version >= 12618.0.0 have android and chrome version
59# info.
60snapshot_cutover_version = '12618.0.0'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080061
Zheng-Jie Changdbe1f8e2021-01-26 12:33:32 +080062# http://crbug.com/1170601, small snapshot ids should be ignored
63# 21000 is R80-12617.0.0
64snapshot_cutover_id = 21000
65
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +080066# current earliest buildbucket buildable versions
67# picked from https://crrev.com/c/2072618
68buildbucket_cutover_versions = [
69 '12931.0.0',
70 '12871.26.0', # R81
71 '12871.24.2', # stabilize-12871.24.B
72 '12812.10.0', # factory-excelsior-12812.B
73 '12768.14.0', # firmware-servo-12768.B
74 '12739.85.0', # R80
75 '12739.67.1', # stabilize-excelsior-12739.67.B
76 '12692.36.0', # factory-hatch-12692.B
77 '12672.104.0', # firmware-hatch-12672.B
78 '12607.110.0', # R79
79 '12607.83.2', # stabilize-quickfix-12607.83.B
80 '12587.59.0', # factory-kukui-12587.B
81 '12573.78.0', # firmware-kukui-12573.B
82 '12499.96.0', # R78
83 '12422.33.0', # firmware-mistral-12422.B
84 '12371.190.0', # R77
85 '12361.38.0', # factory-mistral-12361.B
86 '12200.65.0', # firmware-sarien-12200.B
87 '12105.128.0', # R75
88 '12033.82.0', # factory-sarien-12033.B
89]
90
Kuang-che Wub9705bd2018-06-28 17:59:18 +080091chromeos_root_inside_chroot = '/mnt/host/source'
92# relative to chromeos_root
Kuang-che Wu721e8902021-03-19 12:18:53 +080093in_tree_autotest_dir = 'src/third_party/autotest/files'
Kuang-che Wu7f82c6f2019-08-12 14:29:28 +080094prebuilt_autotest_dir = 'tmp/autotest-prebuilt'
Kuang-che Wu5963ebf2020-10-21 09:01:04 +080095prebuilt_tast_dir = 'tmp/tast-prebuilt'
Kuang-che Wu721e8902021-03-19 12:18:53 +080096# Relative to chromeos root. Images are build_images_dir/$board/$image_name.
97build_images_dir = 'src/build/images'
98cached_images_dir = 'tmp/images'
Kuang-che Wu28980b22019-07-31 19:51:45 +080099test_image_filename = 'chromiumos_test_image.bin'
Kuang-che Wu721e8902021-03-19 12:18:53 +0800100sample_partition_filename = 'full_dev_part_KERN.bin.gz'
Kuang-che Wub9705bd2018-06-28 17:59:18 +0800101
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800102VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
103VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
104VERSION_KEY_MILESTONE = 'milestone'
105VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +0800106VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800107VERSION_KEY_ANDROID_BRANCH = 'android_branch'
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +0800108CROSLAND_URL_TEMPLATE = 'https://crosland.corp.google.com/log/%s..%s'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800109
Kuang-che Wu721e8902021-03-19 12:18:53 +0800110autotest_shadow_config = """
111[CROS]
112enable_ssh_tunnel_for_servo: True
113enable_ssh_tunnel_for_chameleon: True
114enable_ssh_connection_for_devserver: True
115enable_ssh_tunnel_for_moblab: True
116"""
117
118
119class ImageType(enum.Enum):
120 """Chrome OS image type
121
122 It describes the image format, not image location.
123 """
124 # Full disk image like chromiumos_test_image.bin and
125 # chromiumos_test_image.tar.xz.
126 # Supported by 'cros flash'.
127 DISK_IMAGE = enum.auto()
128 # Contains files like full_dev_part_KERN.bin.gz.
129 # Supported by quick-provision and newer 'cros flash'.
130 PARTITION_IMAGE = enum.auto()
131 # Contains files like image.zip. We need to unzip first.
132 ZIP_FILE = enum.auto()
133
134
135class ImageInfo(dict):
136 """Image info (dict: image type -> path).
137
138 For a given Chrome OS version, there are several image formats available.
139 This class describes a collection of images for a certain Chrome OS version.
140 cros_flash() or quick_provision() can resolve a compatible image from this
141 object. `image type` is an ImageType enum. `path` could be a path on the
142 local disk or a remote URI.
143 """
144
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800145
Kuang-che Wu9890ce82018-07-07 15:14:10 +0800146class NeedRecreateChrootException(Exception):
147 """Failed to build ChromeOS because of chroot mismatch or corruption"""
148
149
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800150def is_cros_short_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800151 """Determines if `s` is chromeos short version.
152
153 This function doesn't accept version number of local build.
154 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800155 return bool(re.match(re_chromeos_short_version, s))
156
157
158def is_cros_full_version(s):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800159 """Determines if `s` is chromeos full version.
160
161 This function doesn't accept version number of local build.
162 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800163 return bool(re.match(re_chromeos_full_version, s))
164
165
166def is_cros_version(s):
167 """Determines if `s` is chromeos version (either short or full)"""
168 return is_cros_short_version(s) or is_cros_full_version(s)
169
170
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800171def is_cros_snapshot_version(s):
172 """Determines if `s` is chromeos snapshot version"""
173 return bool(re.match(re_chromeos_snapshot_version, s))
174
175
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800176def is_cros_version_lesseq(ver1, ver2):
177 """Determines if ver1 is less or equal to ver2.
178
179 Args:
180 ver1: a Chrome OS version in short, full, or snapshot format.
181 ver2: a Chrome OS version in short, full, or snapshot format.
182
183 Returns:
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800184 True if ver1 is less or equal to ver2.
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800185 """
186 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
187 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
188
Kuang-che Wu430c5282021-01-27 21:10:25 +0800189 # Compare milestone if available.
190 m1 = re.match(r'R(\d+)', ver1)
191 m2 = re.match(r'R(\d+)', ver2)
192 if m1 and m2 and int(m1.group(1)) > int(m2.group(1)):
193 return False
194
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800195 ver1 = [int(x) for x in re.split(r'[.-]', ver1) if not x.startswith('R')]
196 ver2 = [int(x) for x in re.split(r'[.-]', ver2) if not x.startswith('R')]
197 return ver1 <= ver2
198
199
Kuang-che Wu430c5282021-01-27 21:10:25 +0800200def is_ancestor_version(ver1, ver2):
201 """Determines `ver1` version is ancestor of `ver2` version.
202
203 Returns:
204 True only if `ver1` is the ancestor of `ver2`. One version is not considered
205 as ancestor of itself.
206 """
207 assert is_cros_version(ver1) or is_cros_snapshot_version(ver1)
208 assert is_cros_version(ver2) or is_cros_snapshot_version(ver2)
209
210 if is_cros_version_lesseq(ver2, ver1): # pylint: disable=arguments-out-of-order
211 return False
212 if not is_cros_version_lesseq(ver1, ver2):
213 return False
214
215 if not util.is_direct_relative_version(
216 version_to_short(ver1), version_to_short(ver2)):
217 return False
218
219 # Compare snapshot id if available.
220 if is_cros_snapshot_version(ver1) and is_cros_snapshot_version(ver2):
221 _, short_1, snapshot_1 = snapshot_version_split(ver1)
222 _, short_2, snapshot_2 = snapshot_version_split(ver2)
223 if short_1 == short_2 and snapshot_1 >= snapshot_2:
224 return False
225
226 return True
227
228
Zheng-Jie Chang61a645a2020-04-29 10:12:13 +0800229def is_buildbucket_buildable(version):
230 """Determines if a version is buildable on buildbucket."""
231 short_version = version_to_short(version)
232 # If given version is child of any cutover, then it's buildable
233 return any([
234 util.is_direct_relative_version(x, short_version) and
235 is_cros_version_lesseq(x, version) for x in buildbucket_cutover_versions
236 ])
237
238
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800239def make_cros_full_version(milestone, short_version):
240 """Makes full_version from milestone and short_version"""
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800241 assert milestone
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800242 return 'R%s-%s' % (milestone, short_version)
243
244
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800245def make_cros_snapshot_version(milestone, short_version, snapshot_id):
246 """Makes snapshot version from milestone, short_version and snapshot id"""
247 return 'R%s-%s-%s' % (milestone, short_version, snapshot_id)
248
249
250def version_split(version):
251 """Splits full_version or snapshot_version into milestone and short_version"""
252 assert is_cros_full_version(version) or is_cros_snapshot_version(version)
253 if is_cros_snapshot_version(version):
254 return snapshot_version_split(version)[0:2]
255 milestone, short_version = version.split('-')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800256 return milestone[1:], short_version
257
258
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800259def snapshot_version_split(snapshot_version):
260 """Splits snapshot_version into milestone, short_version and snapshot_id"""
261 assert is_cros_snapshot_version(snapshot_version)
Kuang-che Wu430c5282021-01-27 21:10:25 +0800262 milestone, short_version, snapshot_id = snapshot_version.split('-')
263 return milestone[1:], short_version, snapshot_id
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800264
265
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800266def query_snapshot_buildbucket_id(board, snapshot_version):
267 """Query buildbucket id of a snapshot"""
268 assert is_cros_snapshot_version(snapshot_version)
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800269 path = ('gs://chromeos-image-archive/{board}-snapshot'
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800270 '/{snapshot_version}-*/image.zip')
271 output = gsutil_ls(
272 '-d',
273 path.format(board=board, snapshot_version=snapshot_version),
274 ignore_errors=True)
275 for line in output:
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800276 m = re.match(r'.*-snapshot/R\d+-\d+\.\d+\.\d+-\d+-(.+)/image\.zip', line)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800277 if m:
278 return m.group(1)
279 return None
280
281
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800282def argtype_cros_version(s):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800283 if (not is_cros_version(s)) and (not is_cros_snapshot_version(s)):
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800284 msg = 'invalid cros version'
Kuang-che Wuce2f3be2019-10-28 19:44:54 +0800285 raise cli.ArgTypeError(msg, '9876.0.0, R62-9876.0.0 or R77-12369.0.0-11681')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800286 return s
287
288
289def query_dut_lsb_release(host):
290 """Query /etc/lsb-release of given DUT
291
292 Args:
293 host: the DUT address
294
295 Returns:
Kuang-che Wu3eb6b502018-06-06 16:15:18 +0800296 dict for keys and values of /etc/lsb-release.
297
298 Raises:
Kuang-che Wu44278142019-03-04 11:33:57 +0800299 errors.SshConnectionError: cannot connect to host
300 errors.ExternalError: lsb-release file doesn't exist
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800301 """
302 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800303 output = util.ssh_cmd(host, 'cat', '/etc/lsb-release', allow_retry=True)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800304 except subprocess.CalledProcessError as e:
305 raise errors.ExternalError(
306 'unable to read /etc/lsb-release; not a DUT') from e
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800307 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
308
309
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800310def query_dut_os_release(host):
311 """Query /etc/os-release of given DUT
312
313 Args:
314 host: the DUT address
315
316 Returns:
317 dict for keys and values of /etc/os-release.
318
319 Raises:
320 errors.SshConnectionError: cannot connect to host
321 errors.ExternalError: lsb-release file doesn't exist
322 """
323 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800324 output = util.ssh_cmd(host, 'cat', '/etc/os-release', allow_retry=True)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +0800325 except subprocess.CalledProcessError as e:
326 raise errors.ExternalError(
327 'unable to read /etc/os-release; not a DUT') from e
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800328 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
329
330
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800331def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800332 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800333
334 Args:
335 host: the DUT address
336
337 Returns:
338 True if the host is a chromeos device.
339 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800340 try:
Kuang-che Wuf134ebf2020-08-18 21:06:47 +0800341 return query_dut_os_release(host).get('ID') in [
342 'chromiumos',
343 'chromeos',
Kuang-che Wu44278142019-03-04 11:33:57 +0800344 ]
345 except (errors.ExternalError, errors.SshConnectionError):
346 return False
347
348
349def is_good_dut(host):
350 if not is_dut(host):
351 return False
352
353 # Sometimes python is broken after 'cros flash'.
354 try:
Kuang-che Wu342d3e02021-02-19 15:40:57 +0800355 util.ssh_cmd(host, 'python', '-c', '1', allow_retry=True)
Kuang-che Wu44278142019-03-04 11:33:57 +0800356 return True
357 except (subprocess.CalledProcessError, errors.SshConnectionError):
358 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800359
360
361def query_dut_board(host):
362 """Query board name of a given DUT"""
363 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
364
365
366def query_dut_short_version(host):
Kuang-che Wuacb6efd2018-04-25 18:52:58 +0800367 """Query short version of a given DUT.
368
369 This function may return version of local build, which
370 is_cros_short_version() is false.
371 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800372 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
373
374
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800375def query_dut_prebuilt_version(host):
376 """Return a snapshot version or short version of a given DUT.
377
378 Args:
379 host: dut host
380
381 Returns:
382 Snapshot version or short version.
383 """
384 lsb_release = query_dut_lsb_release(host)
385 release_version = lsb_release.get('CHROMEOS_RELEASE_VERSION')
Kuang-che Wu25fec6f2021-01-28 12:40:43 +0800386 builder_path = lsb_release.get('CHROMEOS_RELEASE_BUILDER_PATH', '')
Zheng-Jie Chang26084542020-04-29 06:34:33 +0800387 match = re.match(r'\S+-(?:snapshot|postsubmit)/(R\d+-\d+\.\d+\.\d+-\d+)-\d+',
388 builder_path)
389 if match:
390 return match.group(1)
391 return release_version
392
393
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800394def query_dut_is_by_official_builder(host):
Kuang-che Wuc092bd52021-01-05 14:25:56 +0800395 """Query if given DUT is build by official builder"""
396 build_type = query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BUILD_TYPE',
397 '')
398 build_type = build_type.split(' - ')[0]
399 assert build_type in ('Official Build', 'Continuous Builder',
400 'Developer Build',
401 'Test Build'), 'unknown build type (%s)' % build_type
402 return build_type in ['Official Build', 'Continuous Builder']
Zheng-Jie Chang181be6f2020-03-17 16:16:08 +0800403
404
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800405def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800406 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800407
408 Args:
409 host: DUT address
410 connect_timeout: connection timeout
411
412 Returns:
413 boot uuid
414 """
Kuang-che Wu44278142019-03-04 11:33:57 +0800415 return util.ssh_cmd(
416 host,
417 'cat',
418 '/proc/sys/kernel/random/boot_id',
419 connect_timeout=connect_timeout).strip()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800420
421
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800422def reboot(host, force_reboot_callback=None):
423 """Reboot a DUT and verify.
424
425 Args:
426 host: DUT address
427 force_reboot_callback: powerful reboot hook (via servo). This will be
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -0700428 invoked if normal reboot failed.
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800429 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800430 logger.debug('reboot %s', host)
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800431 boot_id = None
Kuang-che Wu44278142019-03-04 11:33:57 +0800432 try:
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800433 boot_id = query_dut_boot_id(host)
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800434
Kuang-che Wu2534bab2020-10-23 17:37:16 +0800435 try:
436 util.ssh_cmd(host, 'reboot')
437 except errors.SshConnectionError:
438 # Depends on timing, ssh may return failure due to broken pipe, which is
439 # working as intended. Ignore such kind of errors.
440 pass
441
Kuang-che Wu2ac9a922020-09-03 16:50:12 +0800442 wait_reboot_done(host, boot_id)
443 except (errors.SshConnectionError, errors.ExternalError):
444 if force_reboot_callback and force_reboot_callback(host):
445 wait_reboot_done(host, boot_id)
446 return
447 raise
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800448
Kuang-che Wu708310b2018-03-28 17:24:34 +0800449
450def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800451 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800452 # (dev screen short delay) or more (long delay).
453 time.sleep(15)
454 for _ in range(100):
455 try:
456 # During boot, DUT does not response and thus ssh may hang a while. So
457 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
458 # set tight limit because it's inside retry loop.
459 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
460 return
Kuang-che Wu5f662e82019-03-05 11:49:56 +0800461 except errors.SshConnectionError:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800462 logger.debug('reboot not ready? sleep wait 1 sec')
463 time.sleep(1)
464
Kuang-che Wue121fae2018-11-09 16:18:39 +0800465 raise errors.ExternalError('reboot failed?')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800466
467
468def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800469 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800470
471 Args:
472 args: command line arguments passed to gsutil
473 kwargs:
474 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -0700475 but the path not found.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800476
477 Returns:
478 stdout of gsutil
479
480 Raises:
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800481 errors.ExternalError: gsutil failed to run
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800482 subprocess.CalledProcessError: command failed
483 """
484 stderr_lines = []
485 try:
486 return util.check_output(
487 gsutil_bin, *args, stderr_callback=stderr_lines.append)
488 except subprocess.CalledProcessError as e:
489 stderr = ''.join(stderr_lines)
490 if re.search(r'ServiceException:.* does not have .*access', stderr):
Kuang-che Wue121fae2018-11-09 16:18:39 +0800491 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800492 'gsutil failed due to permission. ' +
493 'Run "%s config" and follow its instruction. ' % gsutil_bin +
494 'Fill any string if it asks for project-id')
495 if kwargs.get('ignore_errors'):
496 return ''
497 raise
498 except OSError as e:
499 if e.errno == errno.ENOENT:
Kuang-che Wue121fae2018-11-09 16:18:39 +0800500 raise errors.ExternalError(
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800501 'Unable to run %s. gsutil is not installed or not in PATH?' %
502 gsutil_bin)
503 raise
504
505
506def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800507 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800508
509 Args:
510 args: arguments passed to 'gsutil ls'
511 kwargs: extra parameters, where
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -0700512 ignore_errors: if true, return empty list instead of raising exception,
513 ex. path not found.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800514
515 Returns:
516 list of 'gsutil ls' result. One element for one line of gsutil output.
517
518 Raises:
519 subprocess.CalledProcessError: gsutil failed, usually means path not found
520 """
521 return gsutil('ls', *args, **kwargs).splitlines()
522
523
Kuang-che Wu876a5382020-10-27 14:24:58 +0800524def gsutil_stat_creation_time(*args, **kwargs):
525 """Returns the creation time of a file or multiple files.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800526
527 Args:
528 args: arguments passed to 'gsutil stat'.
529 kwargs: extra parameters for gsutil.
530
531 Returns:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800532 A integer indicates the creation timestamp.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800533
534 Raises:
535 subprocess.CalledProcessError: gsutil failed, usually means path not found
Kuang-che Wu876a5382020-10-27 14:24:58 +0800536 errors.ExternalError: creation time is not found
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800537 """
538 result = -1
539 # Currently we believe stat always returns a UTC time, and strptime also
540 # parses a UTC time by default.
541 time_format = '%a, %d %b %Y %H:%M:%S GMT'
542
543 for line in gsutil('stat', *args, **kwargs).splitlines():
544 if ':' not in line:
545 continue
Kuang-che Wuc89f2a22019-11-26 15:30:50 +0800546 key, value = line.split(':', 1)
547 key, value = key.strip(), value.strip()
Kuang-che Wu876a5382020-10-27 14:24:58 +0800548 if key != 'Creation time':
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800549 continue
550 dt = datetime.datetime.strptime(value, time_format)
Kuang-che Wu72b5a572019-10-29 20:37:57 +0800551 unixtime = int(calendar.timegm(dt.utctimetuple()))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800552 result = max(result, unixtime)
553
554 if result == -1:
Kuang-che Wu876a5382020-10-27 14:24:58 +0800555 raise errors.ExternalError("didn't find creation time")
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800556 return result
557
558
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800559def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800560 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800561
562 Args:
563 board: ChromeOS board name
564 short_version: ChromeOS version number in short format, ex. 9300.0.0
565
566 Returns:
567 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
568 None if failed.
569 """
570 path = gs_archive_path.format(board=board) + '/R*-' + short_version
571 for line in gsutil_ls('-d', path, ignore_errors=True):
572 m = re.search(r'/R(\d+)-', line)
573 if not m:
574 continue
575 return m.group(1)
576
Zheng-Jie Chang4e25a8d2020-01-08 16:00:13 +0800577 logger.debug('unable to query milestone of %s for %s', short_version, board)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800578 return None
579
580
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800581def list_board_names(chromeos_root):
582 """List board names.
583
584 Args:
585 chromeos_root: chromeos tree root
586
587 Returns:
588 list of board names
589 """
590 # Following logic is simplified from chromite/lib/portage_util.py
591 cros_list_overlays = os.path.join(chromeos_root,
592 'chromite/bin/cros_list_overlays')
593 overlays = util.check_output(cros_list_overlays).splitlines()
594 result = set()
595 for overlay in overlays:
596 conf_file = os.path.join(overlay, 'metadata', 'layout.conf')
597 name = None
598 if os.path.exists(conf_file):
599 for line in open(conf_file):
600 m = re.match(r'^repo-name\s*=\s*(\S+)\s*$', line)
601 if m:
602 name = m.group(1)
603 break
604
605 if not name:
606 name_file = os.path.join(overlay, 'profiles', 'repo_name')
607 if os.path.exists(name_file):
Kuang-che Wua5723492019-11-25 20:59:34 +0800608 with open(name_file) as f:
609 name = f.read().strip()
Kuang-che Wu80bf6a52019-05-31 12:48:06 +0800610
611 if name:
612 name = re.sub(r'-private$', '', name)
613 result.add(name)
614
615 return list(result)
616
617
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800618def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800619 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800620
621 Args:
622 board: ChromeOS board name
623 version: ChromeOS version number in short or full format
624
625 Returns:
626 (milestone, version in short format)
627 """
628 if is_cros_short_version(version):
629 milestone = query_milestone_by_version(board, version)
630 short_version = version
631 else:
632 milestone, short_version = version_split(version)
633 return milestone, short_version
634
635
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800636def extract_major_version(version):
637 """Converts a version to its major version.
638
639 Args:
Kuang-che Wu9501f342019-11-15 17:15:21 +0800640 version: ChromeOS version number or snapshot version
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800641
642 Returns:
643 major version number in string format
644 """
645 version = version_to_short(version)
646 m = re.match(r'^(\d+)\.\d+\.\d+$', version)
647 return m.group(1)
648
649
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800650def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800651 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800652
653 Args:
654 version: ChromeOS version number in short or full format
655
656 Returns:
657 version number in short format
658 """
659 if is_cros_short_version(version):
660 return version
661 _, short_version = version_split(version)
662 return short_version
663
664
665def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800666 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800667
668 Args:
669 board: ChromeOS board name
670 version: ChromeOS version number in short or full format
671
672 Returns:
673 version number in full format
674 """
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800675 if is_cros_snapshot_version(version):
676 milestone, short_version, _ = snapshot_version_split(version)
677 return make_cros_full_version(milestone, short_version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800678 if is_cros_full_version(version):
679 return version
680 milestone = query_milestone_by_version(board, version)
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800681 if not milestone:
682 raise errors.ExternalError('incorrect board=%s or version=%s ?' %
683 (board, version))
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800684 return make_cros_full_version(milestone, version)
685
686
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800687def list_snapshots_from_image_archive(board, major_version):
Kuang-che Wu9501f342019-11-15 17:15:21 +0800688 """List ChromeOS snapshot image available from gs://chromeos-image-archive.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800689
690 Args:
691 board: ChromeOS board
692 major_version: ChromeOS major version
693
694 Returns:
695 list of (version, gs_path):
696 version: Chrome OS snapshot version
697 gs_path: gs path of test image
698 """
699
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800700 def extract_snapshot_id(result):
701 m = re.match(r'^R\d+-\d+\.\d+\.\d+-(\d+)', result[0])
702 assert m
703 return int(m.group(1))
704
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800705 short_version = '%s.0.0' % major_version
706 milestone = query_milestone_by_version(board, short_version)
707 if not milestone:
708 milestone = '*'
709
Zheng-Jie Chang54020832020-04-21 15:52:48 +0800710 path = ('gs://chromeos-image-archive/{board}-snapshot/R{milestone}-'
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800711 '{short_version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800712 result = []
713 output = gsutil_ls(
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800714 path.format(
715 board=board, milestone=milestone, short_version=short_version),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800716 ignore_errors=True)
717
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800718 for gs_path in sorted(output):
719 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)', gs_path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800720 if m:
721 snapshot_version = m.group(1)
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800722 # we should skip if there is duplicate snapshot
723 if result and result[-1][0] == snapshot_version:
724 continue
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800725
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800726 _, _, snapshot_id = snapshot_version_split(snapshot_version)
Zheng-Jie Changdbe1f8e2021-01-26 12:33:32 +0800727
728 # crbug/1170601: ignore small snapshot ids
729 if int(snapshot_id) <= snapshot_cutover_id:
730 continue
731
732 # b/151054108: snapshot version in [29288, 29439] is broken
Zheng-Jie Chang1ae64c32020-03-10 14:08:28 +0800733 if 29288 <= int(snapshot_id) <= 29439:
734 continue
735
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800736 result.append((snapshot_version, gs_path))
Zheng-Jie Chang43a08412019-12-05 17:05:45 +0800737
738 # sort by its snapshot_id
739 result.sort(key=extract_snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800740 return result
741
742
Kuang-che Wu575dc442019-03-05 10:30:55 +0800743def list_prebuilt_from_image_archive(board):
744 """Lists ChromeOS prebuilt image available from gs://chromeos-image-archive.
745
Kuang-che Wu575dc442019-03-05 10:30:55 +0800746 Args:
747 board: ChromeOS board name
748
749 Returns:
750 list of (version, gs_path):
751 version: Chrome OS version in full format
752 gs_path: gs path of test image
753 """
754 result = []
755 for line in gsutil_ls(gs_archive_path.format(board=board)):
756 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+)', line)
757 if m:
758 full_version = m.group(1)
759 test_image = 'chromiumos_test_image.tar.xz'
760 assert line.endswith('/')
761 gs_path = line + test_image
762 result.append((full_version, gs_path))
763 return result
764
765
Kuang-che Wue1808402020-01-06 20:27:45 +0800766def has_test_image(board, version):
767 if is_cros_snapshot_version(version):
768 return bool(query_snapshot_buildbucket_id(board, version))
769
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800770 try:
771 full_version = version_to_full(board, version)
772 except errors.ExternalError:
773 # version_to_full() is implemented by checking image, thus its failure
774 # means no image.
775 return False
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800776 path = (
Kuang-che Wue1808402020-01-06 20:27:45 +0800777 gs_archive_path.format(board=board) +
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800778 '/%s/chromiumos_test_image.tar.xz' % full_version)
Kuang-che Wue1808402020-01-06 20:27:45 +0800779
Kuang-che Wuef3f37c2021-03-08 16:30:38 +0800780 if gsutil_ls(path, ignore_errors=True):
781 return True
Kuang-che Wue1808402020-01-06 20:27:45 +0800782 return False
783
784
Kuang-che Wu575dc442019-03-05 10:30:55 +0800785def list_chromeos_prebuilt_versions(board,
786 old,
787 new,
788 only_good_build=True,
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800789 use_snapshot=False):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800790 """Lists ChromeOS version numbers with prebuilt between given range
791
792 Args:
793 board: ChromeOS board name
794 old: start version (inclusive)
795 new: end version (inclusive)
796 only_good_build: only if test image is available
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800797 use_snapshot: return snapshot versions if found
Kuang-che Wu575dc442019-03-05 10:30:55 +0800798
799 Returns:
800 list of sorted version numbers (in full format) between [old, new] range
801 (inclusive).
802 """
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800803 old_short = version_to_short(old)
804 new_short = version_to_short(new)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800805
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800806 rev_map = {
807 } # dict: short version -> list of (short/full or snapshot version, gs path)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800808 for full_version, gs_path in list_prebuilt_from_image_archive(board):
809 short_version = version_to_short(full_version)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800810 rev_map[short_version] = [(full_version, gs_path)]
Kuang-che Wu575dc442019-03-05 10:30:55 +0800811
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800812 if use_snapshot:
813 for major_version in range(
814 int(extract_major_version(old)),
815 int(extract_major_version(new)) + 1):
816 short_version = '%s.0.0' % major_version
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800817 next_short_version = '%s.0.0' % (major_version + 1)
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800818 # If current version is smaller than cutover, ignore it as it might not
819 # contain enough information for continuing android and chrome bisection.
820 if not util.is_version_lesseq(snapshot_cutover_version, short_version):
821 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800822
823 # Given the fact that snapshots are images between two release versions.
824 # Adding snapshots of 12345.0.0 should be treated as adding commits
825 # between [12345.0.0, 12346.0.0).
826 # So in the following lines we check two facts:
827 # 1) If 12346.0.0(next_short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800828 if not util.is_direct_relative_version(next_short_version, old_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800829 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800830 if not util.is_direct_relative_version(next_short_version, new_short):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800831 continue
832 # 2) If 12345.0.0(short_version) is a version between old and new
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800833 if not util.is_direct_relative_version(short_version, old_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800834 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800835 if not util.is_direct_relative_version(short_version, new_short):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800836 continue
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800837
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800838 snapshots = list_snapshots_from_image_archive(board, str(major_version))
839 if snapshots:
840 # if snapshots found, we can append them after the release version,
841 # so the prebuilt image list of this version will be
842 # release_image, snapshot1, snapshot2,...
Zheng-Jie Changb8697042019-10-29 16:03:26 +0800843 if short_version not in rev_map:
844 rev_map[short_version] = []
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800845 rev_map[short_version] += snapshots
Kuang-che Wu575dc442019-03-05 10:30:55 +0800846
847 result = []
848 for rev in sorted(rev_map, key=util.version_key_func):
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800849 if not util.is_direct_relative_version(new_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800850 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800851 if not util.is_version_lesseq(old_short, rev):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800852 continue
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800853 if not util.is_version_lesseq(rev, new_short):
Kuang-che Wu575dc442019-03-05 10:30:55 +0800854 continue
855
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800856 for version, gs_path in rev_map[rev]:
Kuang-che Wu575dc442019-03-05 10:30:55 +0800857
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800858 # version_to_full() and gsutil_ls() may take long time if versions are a
859 # lot. This is acceptable because we usually bisect only short range.
Kuang-che Wu575dc442019-03-05 10:30:55 +0800860
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800861 if only_good_build and not is_cros_snapshot_version(version):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800862 gs_result = gsutil_ls(gs_path, ignore_errors=True)
863 if not gs_result:
864 logger.warning('%s is not a good build, ignore', version)
865 continue
866 assert len(gs_result) == 1
867 m = re.search(r'(R\d+-\d+\.\d+\.\d+)', gs_result[0])
868 if not m:
869 logger.warning('format of image path is unexpected: %s', gs_result[0])
870 continue
Zheng-Jie Changa331b872020-01-06 17:09:32 +0800871 version = m.group(1)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800872 elif is_cros_short_version(version):
873 version = version_to_full(board, version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800874
Zheng-Jie Changaea4fba2020-02-17 17:12:09 +0800875 if is_cros_version_lesseq(old, version) and is_cros_version_lesseq(
876 version, new):
877 result.append(version)
Kuang-che Wu575dc442019-03-05 10:30:55 +0800878
879 return result
880
881
Kuang-che Wu721e8902021-03-19 12:18:53 +0800882def search_snapshot_image(board, snapshot_version):
883 """Searches chromeos snapshot image.
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800884
885 Args:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800886 board: ChromeOS board name
887 snapshot_version: ChromeOS snapshot version number
888
889 Returns:
Kuang-che Wu721e8902021-03-19 12:18:53 +0800890 ImageInfo object
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800891 """
892 assert is_cros_snapshot_version(snapshot_version)
Kuang-che Wu721e8902021-03-19 12:18:53 +0800893 image_info = ImageInfo()
894 gs_path = gs_archive_base + '{board}-snapshot/{snapshot_version}-*'.format(
895 board=board, snapshot_version=snapshot_version)
896 files = gsutil_ls(
897 gs_path + '/' + sample_partition_filename, ignore_errors=True)
898 if files:
899 image_info[ImageType.PARTITION_IMAGE] = files[0].replace(
900 sample_partition_filename, '')
901 files = gsutil_ls(gs_path + '/image.zip', ignore_errors=True)
902 if files:
903 image_info[ImageType.ZIP_FILE] = files[0]
904 return image_info
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800905
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800906
Kuang-che Wu721e8902021-03-19 12:18:53 +0800907def prepare_image_for_quick_provision(image_info):
908 path = image_info.get(ImageType.PARTITION_IMAGE)
909 if path and path.startswith(gs_archive_base):
910 return urllib.parse.urlparse(path).path[1:]
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +0800911
Kuang-che Wu721e8902021-03-19 12:18:53 +0800912 logger.warning(
913 'image format or location are not supported by quick-provision: %s',
914 image_info)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +0800915 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +0800916
917
Kuang-che Wu721e8902021-03-19 12:18:53 +0800918def _cache_path_for_download(chromeos_root, url):
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800919 os.makedirs(os.path.join(chromeos_root, cached_images_dir), exist_ok=True)
Kuang-che Wu721e8902021-03-19 12:18:53 +0800920 name = urllib.parse.quote(url, safe='')
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800921 return os.path.join(cached_images_dir, name)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800922
Kuang-che Wu721e8902021-03-19 12:18:53 +0800923
924def prepare_image_for_cros_flash(chromeos_root, image_info):
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800925 """Prepares image path for 'cros flash'.
926
927 Returns:
928 path recognized by 'cros flash'. Local disk path will be relative to
929 chromeos_root.
930 """
Kuang-che Wu721e8902021-03-19 12:18:53 +0800931 path = image_info.get(ImageType.DISK_IMAGE)
932 if path:
933 # local path
934 if '://' not in path:
935 return path
936
937 m = re.search(gs_archive_base + r'([^/]+)-[a-z]+/([^/]+)/', path)
938 if m:
939 return 'xbuddy://remote/%s/%s/test' % (m.group(1), m.group(2))
940 if path.startswith(gs_archive_base):
941 return path.replace('chromiumos_test_image.tar.xz', 'test')
942
943 # 'cros flash' doesn't support other gs bucket, download to local.
944 if path.startswith('gs://'):
945 cache_path = _cache_path_for_download(chromeos_root, path)
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800946 cache_path_full = os.path.join(chromeos_root, cache_path)
947 if os.path.exists(cache_path_full):
Kuang-che Wu721e8902021-03-19 12:18:53 +0800948 return cache_path
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800949 gsutil('cp', path, cache_path_full)
Kuang-che Wu721e8902021-03-19 12:18:53 +0800950 return cache_path
951
952 path = image_info.get(ImageType.PARTITION_IMAGE)
953 if path and path.startswith(gs_archive_base):
954 # newer 'cros flash' support partition images
Kuang-che Wu721e8902021-03-19 12:18:53 +0800955 if git_util.is_ancestor_commit(
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800956 os.path.join(chromeos_root, 'chromite'), '191e7333cbeb7b', 'HEAD'):
Kuang-che Wu721e8902021-03-19 12:18:53 +0800957 return path
958
959 path = image_info.get(ImageType.ZIP_FILE)
960 if path:
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800961 cache_path = _cache_path_for_download(chromeos_root,
962 path + '.' + test_image_filename)
963 cache_path_full = os.path.join(chromeos_root, cache_path)
964 if os.path.exists(cache_path_full):
965 return cache_path_full
Kuang-che Wu721e8902021-03-19 12:18:53 +0800966
967 tmp_dir = tempfile.mkdtemp()
968 try:
969 if path.startswith('gs://'):
970 gsutil('cp', path, tmp_dir)
971 path = os.path.join(tmp_dir, os.path.basename(path))
972 assert os.path.exists(path)
973 util.check_call('unzip', '-j', path, test_image_filename, cwd=tmp_dir)
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800974 shutil.move(os.path.join(tmp_dir, test_image_filename), cache_path_full)
Kuang-che Wu721e8902021-03-19 12:18:53 +0800975 finally:
976 shutil.rmtree(tmp_dir)
977
Kuang-che Wu4acd4122021-04-21 11:56:15 +0800978 return cache_path
Kuang-che Wu721e8902021-03-19 12:18:53 +0800979
980 return None
981
982
983def quick_provision(chromeos_root, host, image_info):
984 # TODO(kimjae): Transition to using TLS ProvisionDut for F20.
985 logger.debug('quick_provision %s %s', host, image_info)
986 build = prepare_image_for_quick_provision(image_info)
987 if not build:
988 return False
989
990 autotest_path = os.path.join(chromeos_root_inside_chroot,
991 in_tree_autotest_dir)
992 quick_provision_cmd = [
993 'test_that', '--args',
994 "value='%s'" % build, host, 'provision_QuickProvision', '--autotest_dir',
Jae Hoon Kimfc982a92021-04-09 13:31:05 -0700995 autotest_path, '--debug'
Kuang-che Wu721e8902021-03-19 12:18:53 +0800996 ]
997 try:
998 cros_sdk(chromeos_root, *quick_provision_cmd)
999 except subprocess.CalledProcessError as e:
1000 raise errors.ExternalError('quick-provision failed') from e
1001 return True
1002
1003
1004def verify_dut_version(host, board, version):
1005 if version:
1006 # In the past, cros flash may fail with returncode=0
1007 # So let's have an extra check.
1008 if is_cros_snapshot_version(version):
1009 builder_path = query_dut_lsb_release(host).get(
1010 'CHROMEOS_RELEASE_BUILDER_PATH', '')
1011 expect_prefix = '%s-snapshot/%s-' % (board, version)
1012 if not builder_path.startswith(expect_prefix):
1013 raise errors.ExternalError(
1014 'although provision succeeded, the OS builder path is '
1015 'unexpected: actual=%s expect=%s' % (builder_path, expect_prefix))
1016 else:
1017 expect_version = version_to_short(version)
1018 dut_version = query_dut_short_version(host)
1019 if dut_version != expect_version:
1020 raise errors.ExternalError(
1021 'although provision succeeded, the OS version is unexpected: '
1022 'actual=%s expect=%s' % (dut_version, expect_version))
1023
1024 # "cros flash" may terminate successfully but the DUT starts self-repairing
1025 # (b/130786578), so it's necessary to do sanity check.
1026 if not is_good_dut(host):
1027 raise errors.ExternalError(
1028 'although provision succeeded, the DUT is in bad state')
1029
1030
1031def provision_image(chromeos_root,
1032 host,
1033 board,
1034 image_info,
1035 version=None,
1036 clobber_stateful=False,
1037 disable_rootfs_verification=True,
1038 force_reboot_callback=None):
1039 # Try quick_provision first, but fallback to cros flash.
1040 # TODO(kcwu): only use quick_provision for DUTs in the lab
1041 try:
1042 if quick_provision(chromeos_root, host, image_info):
1043 verify_dut_version(host, board, version)
1044 return
1045 logger.debug('quick-provision is not supported; fallback to cros flash')
1046 except errors.ExternalError as e:
1047 logger.warning('quick-provision failed; fallback to cros flash: %s', e)
1048
1049 if not cros_flash(
1050 chromeos_root,
1051 host,
1052 image_info,
1053 clobber_stateful=clobber_stateful,
1054 disable_rootfs_verification=disable_rootfs_verification,
1055 force_reboot_callback=force_reboot_callback):
1056 raise errors.InternalError('unsupported image: ' + str(image_info))
1057 verify_dut_version(host, board, version)
1058
1059
1060def search_prebuilt_image(board, version):
1061 """Searches chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001062
1063 Args:
Kuang-che Wu28980b22019-07-31 19:51:45 +08001064 chromeos_root: chromeos tree root
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001065 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001066 version: ChromeOS version number in short or full format
1067
1068 Returns:
Kuang-che Wu721e8902021-03-19 12:18:53 +08001069 ImageInfo object
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001070 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001071 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001072 full_version = version_to_full(board, version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001073
Kuang-che Wu721e8902021-03-19 12:18:53 +08001074 image_info = ImageInfo()
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001075 gs_path = gs_archive_path.format(board=board) + '/' + full_version
Kuang-che Wu721e8902021-03-19 12:18:53 +08001076 if gsutil_ls(gs_path + '/chromiumos_test_image.tar.xz', ignore_errors=True):
1077 image_info[ImageType.DISK_IMAGE] = gs_path + '/chromiumos_test_image.tar.xz'
1078 if gsutil_ls(gs_path + '/' + sample_partition_filename, ignore_errors=True):
1079 image_info[ImageType.PARTITION_IMAGE] = gs_path
1080 return image_info
1081
1082
1083def search_image(board, version):
1084 if is_cros_snapshot_version(version):
1085 return search_snapshot_image(board, version)
1086 return search_prebuilt_image(board, version)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001087
1088
1089def cros_flash(chromeos_root,
1090 host,
Kuang-che Wu721e8902021-03-19 12:18:53 +08001091 image_info,
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001092 clobber_stateful=False,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001093 disable_rootfs_verification=True,
1094 force_reboot_callback=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001095 """Flash a DUT with given ChromeOS image.
1096
1097 This is implemented by 'cros flash' command line.
1098
1099 Args:
1100 chromeos_root: use 'cros flash' of which chromeos tree
1101 host: DUT address
1102 board: ChromeOS board name
Kuang-che Wu721e8902021-03-19 12:18:53 +08001103 image_info: ImageInfo object
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001104 version: ChromeOS version in short or full format
1105 clobber_stateful: Clobber stateful partition when performing update
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07001106 disable_rootfs_verification: Disable rootfs verification after update is
1107 completed
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001108 force_reboot_callback: powerful reboot hook (via servo)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001109
Kuang-che Wu721e8902021-03-19 12:18:53 +08001110 Returns:
1111 False for unsupported images
1112
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001113 Raises:
1114 errors.ExternalError: cros flash failed
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001115 """
Kuang-che Wu721e8902021-03-19 12:18:53 +08001116 logger.info('cros_flash %s %s', host, image_info)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001117
1118 # Reboot is necessary because sometimes previous 'cros flash' failed and
1119 # entered a bad state.
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001120 reboot(host, force_reboot_callback=force_reboot_callback)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001121
Kuang-che Wu020a1182020-09-08 17:17:22 +08001122 # Stop service ap-update-manager to prevent rebooting during auto update.
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001123 # The service is used in jetstream boards, but not other CrOS devices.
1124 if query_dut_os_release(host).get('GOOGLE_CRASH_ID') == 'Jetstream':
1125 try:
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001126 # Sleep to wait ap-update-manager start, which may take up to 27 seconds.
1127 # For simplicity, we wait 60 seconds here, which is the timeout value of
1128 # jetstream_host.
1129 # https://chromium.googlesource.com/chromiumos/third_party/autotest
Kuang-che Wuebc2c362020-12-14 16:27:09 +08001130 # /+/HEAD/server/hosts/jetstream_host.py#27
Kuang-che Wu29e2eaf2020-08-21 16:51:26 +08001131 time.sleep(60)
Kuang-che Wu2e0680b2020-08-19 22:41:28 +08001132 util.ssh_cmd(host, 'stop', 'ap-update-manager')
1133 except subprocess.CalledProcessError:
1134 pass # not started; do nothing
1135
Kuang-che Wu721e8902021-03-19 12:18:53 +08001136 image_path = prepare_image_for_cros_flash(chromeos_root, image_info)
1137 if not image_path:
1138 return False
1139
Kuang-che Wu28980b22019-07-31 19:51:45 +08001140 # Handle relative path.
1141 if '://' not in image_path and not os.path.isabs(image_path):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001142 image_path = os.path.join(chromeos_root_inside_chroot, image_path)
1143
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001144 args = [
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +08001145 '--debug',
1146 '--no-ping',
1147 # Speed up for slow network connection.
1148 '--send-payload-in-parallel',
1149 host,
1150 image_path,
Kuang-che Wuf3d03ca2019-03-11 17:31:40 +08001151 ]
Kuang-che Wu8a83c9b2021-01-11 14:52:15 +08001152 # TODO(kcwu): remove this check if we don't need to support chromeos versions
1153 # earlier than Dec 2020.
1154 if git_util.is_ancestor_commit(
1155 os.path.join(chromeos_root, 'chromite'), '9ed30bc3ed292b', 'HEAD'):
1156 # To reduce disk usage on DUT.
1157 args.append('--no-copy-payloads-to-device')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001158 if clobber_stateful:
1159 args.append('--clobber-stateful')
1160 if disable_rootfs_verification:
1161 args.append('--disable-rootfs-verification')
1162
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001163 try:
1164 cros_sdk(chromeos_root, 'cros', 'flash', *args)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001165 except subprocess.CalledProcessError as e:
1166 raise errors.ExternalError('cros flash failed') from e
Kuang-che Wu721e8902021-03-19 12:18:53 +08001167 return True
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001168
1169
Kuang-che Wu721e8902021-03-19 12:18:53 +08001170def provision_image_with_retry(chromeos_root,
1171 host,
1172 board,
1173 image_info,
1174 version=None,
1175 clobber_stateful=False,
1176 disable_rootfs_verification=True,
1177 repair_callback=None,
1178 force_reboot_callback=None):
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001179 # 'cros flash' is not 100% reliable, retry if necessary.
1180 for attempt in range(2):
1181 if attempt > 0:
1182 logger.info('will retry 60 seconds later')
1183 time.sleep(60)
1184
1185 try:
Kuang-che Wu721e8902021-03-19 12:18:53 +08001186 provision_image(
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001187 chromeos_root,
1188 host,
1189 board,
Kuang-che Wu721e8902021-03-19 12:18:53 +08001190 image_info,
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001191 version=version,
1192 clobber_stateful=clobber_stateful,
Kuang-che Wu2ac9a922020-09-03 16:50:12 +08001193 disable_rootfs_verification=disable_rootfs_verification,
1194 force_reboot_callback=force_reboot_callback)
Zheng-Jie Changfc5d8742021-03-31 04:57:43 +08001195 workaround_b183567529(
1196 host, board, version, force_reboot_callback=force_reboot_callback)
Kuang-che Wu414d67f2019-05-28 11:28:57 +08001197 return True
1198 except errors.ExternalError:
1199 logger.exception('cros flash failed')
1200 if repair_callback and not repair_callback(host):
1201 logger.warning('not repaired, assume it is harmless')
1202 continue
1203 return False
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001204
1205
1206def version_info(board, version):
1207 """Query subcomponents version info of given version of ChromeOS
1208
1209 Args:
1210 board: ChromeOS board name
1211 version: ChromeOS version number in short or full format
1212
1213 Returns:
1214 dict of component and version info, including (if available):
1215 cros_short_version: ChromeOS version
1216 cros_full_version: ChromeOS version
1217 milestone: milestone of ChromeOS
1218 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +08001219 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001220 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
1221 """
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001222 if is_cros_snapshot_version(version):
Zheng-Jie Chang2b6d1472019-11-13 12:40:17 +08001223 api = buildbucket_util.BuildbucketApi()
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001224 milestone, short_version, _ = snapshot_version_split(version)
1225 buildbucket_id = query_snapshot_buildbucket_id(board, version)
Zheng-Jie Chang597c38b2020-03-12 22:24:10 +08001226 data = api.get_build(int(buildbucket_id)).output.properties
Zheng-Jie Chang4fabff62019-12-08 21:54:35 +08001227 target_versions = json_format.MessageToDict(data['target_versions'])
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001228 return {
Zheng-Jie Changb8697042019-10-29 16:03:26 +08001229 VERSION_KEY_MILESTONE: milestone,
1230 VERSION_KEY_CROS_FULL_VERSION: version,
1231 VERSION_KEY_CROS_SHORT_VERSION: short_version,
Zheng-Jie Chang2d1dd9b2019-12-07 23:30:20 +08001232 VERSION_KEY_CR_VERSION: target_versions.get('chromeVersion'),
1233 VERSION_KEY_ANDROID_BUILD_ID: target_versions.get('androidVersion'),
1234 VERSION_KEY_ANDROID_BRANCH: target_versions.get('androidBranchVersion'),
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001235 }
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001236 info = {}
1237 full_version = version_to_full(board, version)
1238
1239 # Some boards may have only partial-metadata.json but no metadata.json.
1240 # e.g. caroline R60-9462.0.0
1241 # Let's try both.
1242 metadata = None
1243 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
Kuang-che Wu0768b972019-10-05 15:18:59 +08001244 path = gs_archive_path.format(
1245 board=board) + '/%s/%s' % (full_version, metadata_filename)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001246 metadata = gsutil('cat', path, ignore_errors=True)
1247 if metadata:
1248 o = json.loads(metadata)
1249 v = o['version']
1250 board_metadata = o['board-metadata'][board]
1251 info.update({
1252 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
1253 VERSION_KEY_CROS_FULL_VERSION: v['full'],
1254 VERSION_KEY_MILESTONE: v['milestone'],
1255 VERSION_KEY_CR_VERSION: v['chrome'],
1256 })
1257
1258 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +08001259 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001260 if 'android-branch' in v: # this appears since R58-9317.0.0
1261 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
1262 elif 'android-container-branch' in board_metadata:
1263 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
1264 break
1265 else:
1266 logger.error('Failed to read metadata from gs://chromeos-image-archive')
1267 logger.error(
1268 'Note, so far no quick way to look up version info for too old builds')
1269
1270 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001271
1272
1273def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001274 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +08001275
1276 Args:
1277 board: ChromeOS board name
1278 version: ChromeOS version number in short or full format
1279
1280 Returns:
1281 Chrome version number
1282 """
1283 info = version_info(board, version)
1284 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +08001285
1286
1287def query_android_build_id(board, rev):
1288 info = version_info(board, rev)
1289 rev = info['android_build_id']
1290 return rev
1291
1292
1293def query_android_branch(board, rev):
1294 info = version_info(board, rev)
1295 rev = info['android_branch']
1296 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001297
1298
Kuang-che Wu3eb6b502018-06-06 16:15:18 +08001299def guess_chrome_version(board, rev):
1300 """Guess chrome version number.
1301
1302 Args:
1303 board: chromeos board name
1304 rev: chrome or chromeos version
1305
1306 Returns:
1307 chrome version number
1308 """
1309 if is_cros_version(rev):
1310 assert board, 'need to specify BOARD for cros version'
1311 rev = query_chrome_version(board, rev)
1312 assert cr_util.is_chrome_version(rev)
1313
1314 return rev
1315
1316
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001317def is_inside_chroot():
1318 """Returns True if we are inside chroot."""
1319 return os.path.exists('/etc/cros_chroot_version')
1320
1321
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001322def convert_path_outside_chroot(chromeos_root, path):
1323 """Converts path in chroot to outside.
1324
1325 Args:
1326 chromeos_root: chromeos tree root
1327 path: path inside chroot; support starting with '~/'
1328
1329 Returns:
1330 The corresponding path outside chroot assuming the chroot is mounted
1331 """
1332 if path.startswith('~/'):
1333 path = path.replace('~', '/home/' + os.environ['USER'])
1334 assert '~' not in path, 'tilde (~) character is not fully supported'
1335
1336 assert os.path.isabs(path)
1337 assert path[0] == os.sep
1338 return os.path.join(chromeos_root, 'chroot', path[1:])
1339
1340
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001341def cros_sdk(chromeos_root,
1342 *args,
1343 chrome_root=None,
1344 env=None,
1345 log_stdout=True,
1346 stdin=None,
1347 stderr_callback=None,
1348 goma_dir=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001349 """Run commands inside chromeos chroot.
1350
1351 Args:
1352 chromeos_root: chromeos tree root
1353 *args: command to run
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001354 chrome_root: pass to cros_sdk; mount this path into the SDK chroot
1355 env: (dict) environment variables for the command
1356 log_stdout: Whether write the stdout output of the child process to log.
1357 stdin: standard input file handle for the command
1358 stderr_callback: Callback function for stderr. Called once per line.
1359 goma_dir: Goma installed directory to mount into the chroot
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001360 """
1361 envs = []
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001362 if env:
1363 for k, v in env.items():
1364 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
1365 envs.append('%s=%s' % (k, v))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001366
1367 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
1368 # commands would be considered as background process.
Kuang-che Wu399d4662019-06-06 15:23:37 +08001369 prefix = ['chromite/bin/cros_sdk', '--no-ns-pid']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001370
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001371 if chrome_root:
1372 prefix += ['--chrome_root', chrome_root]
1373 if goma_dir:
1374 prefix += ['--goma_dir', goma_dir]
Kuang-che Wud4603d72018-11-29 17:51:21 +08001375
Kuang-che Wu399d4662019-06-06 15:23:37 +08001376 prefix += envs + ['--']
Kuang-che Wud4603d72018-11-29 17:51:21 +08001377
Kuang-che Wu399d4662019-06-06 15:23:37 +08001378 # In addition to the output of command we are interested, cros_sdk may
1379 # generate its own messages. For example, chroot creation messages if we run
1380 # cros_sdk the first time.
1381 # This is the hack to run dummy command once, so we can get clean output for
1382 # the command we are interested.
1383 cmd = prefix + ['true']
Kuang-che Wu62677012020-07-13 14:25:18 +08001384 try:
1385 util.check_call(*cmd, cwd=chromeos_root)
1386 except subprocess.CalledProcessError:
1387 logger.exception('cros_sdk init/update failed')
1388 raise
Kuang-che Wu399d4662019-06-06 15:23:37 +08001389
1390 cmd = prefix + list(args)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001391 return util.check_output(
1392 *cmd,
1393 cwd=chromeos_root,
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001394 log_stdout=log_stdout,
1395 stdin=stdin,
1396 stderr_callback=stderr_callback)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001397
1398
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001399def create_chroot(chromeos_root):
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001400 """Creates ChromeOS chroot if necessary.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001401
1402 Args:
1403 chromeos_root: chromeos tree root
1404 """
1405 if os.path.exists(os.path.join(chromeos_root, 'chroot')):
1406 return
1407 if os.path.exists(os.path.join(chromeos_root, 'chroot.img')):
1408 return
1409
1410 util.check_output('chromite/bin/cros_sdk', '--create', cwd=chromeos_root)
1411
1412
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001413def mount_chroot(chromeos_root):
1414 """Creates ChromeOS chroot if necessary.
1415
1416 Args:
1417 chromeos_root: chromeos tree root
1418 """
1419 # An arbitrary file must exist in chroot.
1420 path = convert_path_outside_chroot(chromeos_root, '/bin/ls')
1421
1422 # Not created or mounted yet.
1423 if not os.path.exists(path):
1424 create_chroot(chromeos_root)
1425 # After this command, the chroot is mounted.
1426 cros_sdk(chromeos_root, 'true')
1427 assert os.path.exists(path)
1428
1429
1430def copy_into_chroot(chromeos_root, src, dst, overwrite=True):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001431 """Copies file into chromeos chroot.
1432
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001433 The side effect is chroot created and mounted.
1434
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001435 Args:
1436 chromeos_root: chromeos tree root
1437 src: path outside chroot
1438 dst: path inside chroot
Kuang-che Wu020a1182020-09-08 17:17:22 +08001439 overwrite: overwrite if dst already exists
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001440 """
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001441 mount_chroot(chromeos_root)
1442 src = os.path.expanduser(src)
1443 dst_outside = convert_path_outside_chroot(chromeos_root, dst)
1444 if not overwrite and os.path.exists(dst_outside):
1445 return
1446
1447 # Haven't support directory or special files yet.
1448 assert os.path.isfile(src)
1449 assert os.path.isfile(dst_outside) or not os.path.exists(dst_outside)
1450
1451 dirname = os.path.dirname(dst_outside)
1452 if not os.path.exists(dirname):
1453 os.makedirs(dirname)
1454 shutil.copy(src, dst_outside)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001455
1456
Kuang-che Wu721e8902021-03-19 12:18:53 +08001457def _copy_template_files(src, dst):
1458 if not os.path.exists(src):
1459 return
1460
1461 def copy_if_nonexistent(src, dst):
1462 if not os.path.exists(dst):
1463 shutil.copy2(src, dst)
1464
1465 shutil.copytree(
1466 src, dst, dirs_exist_ok=True, copy_function=copy_if_nonexistent)
1467
1468
1469def override_autotest_config(autotest_dir):
1470 shadow_config_path = os.path.join(autotest_dir, 'shadow_config.ini')
1471 if not os.path.exists(shadow_config_path):
1472 with open(shadow_config_path, 'w') as f:
1473 f.write(autotest_shadow_config)
1474
1475
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001476def prepare_chroot(chromeos_root):
1477 mount_chroot(chromeos_root)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001478
Kuang-che Wuad5ac7b2020-02-20 19:02:52 +08001479 # Work around b/149077936:
1480 # The creds file is copied into the chroot since 12866.0.0.
1481 # But earlier versions need this file as well because of cipd ACL change.
1482 creds_path = '~/.config/chrome_infra/auth/creds.json'
1483 if os.path.exists(os.path.expanduser(creds_path)):
1484 copy_into_chroot(chromeos_root, creds_path, creds_path, overwrite=False)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001485
Kuang-che Wu721e8902021-03-19 12:18:53 +08001486 # quick-provision requires special config for autotest.
1487 override_autotest_config(os.path.join(chromeos_root, in_tree_autotest_dir))
1488
1489 # Copy optional configure files into the home directory inside chromeos
1490 # chroot. For example, quick-provision may need special ssh config.
1491 assert os.environ.get('USER')
1492 _copy_template_files(
1493 os.path.join(common.BISECT_KIT_ROOT, 'cros_template_files', 'at_home'),
1494 os.path.join(chromeos_root, 'chroot', 'home', os.environ['USER']))
1495
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07001496 # Copy ssh keys into chromeos root.
1497 for name in ['testing_rsa', 'testing_rsa.pub']:
1498 path = os.path.join(chromeos_root, 'src', 'scripts', 'mod_for_test_scripts',
1499 'ssh_keys', name)
1500 shutil.copy(
1501 path,
Kuang-che Wu6849f2d2021-04-22 02:23:04 +08001502 os.path.join(chromeos_root, 'chroot', 'home', os.environ['USER'],
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07001503 '.ssh'))
1504
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001505
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001506def check_if_need_recreate_chroot(stdout, stderr):
1507 """Analyze build log and determine if chroot should be recreated.
1508
1509 Args:
1510 stdout: stdout output of build
1511 stderr: stderr output of build
1512
1513 Returns:
1514 the reason if chroot needs recreated; None otherwise
1515 """
Kuang-che Wu74768d32018-09-07 12:03:24 +08001516 if re.search(
1517 r"The current version of portage supports EAPI '\d+'. "
Kuang-che Wuae6824b2019-08-27 22:20:01 +08001518 'You must upgrade', stderr):
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001519 return 'EAPI version mismatch'
1520
Kuang-che Wu5ac81322018-11-26 14:04:06 +08001521 if 'Chroot is too new. Consider running:' in stderr:
1522 return 'chroot version is too new'
1523
1524 # old message before Oct 2018
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001525 if 'Chroot version is too new. Consider running cros_sdk --replace' in stderr:
1526 return 'chroot version is too new'
1527
Kuang-che Wu6fe987f2018-08-28 15:24:20 +08001528 # https://groups.google.com/a/chromium.org/forum/#!msg/chromium-os-dev/uzwT5APspB4/NFakFyCIDwAJ
1529 if "undefined reference to 'std::__1::basic_string" in stdout:
1530 return 'might be due to compiler change'
1531
Kuang-che Wu94e3b452019-11-21 12:49:18 +08001532 # Detect failures due to file collisions.
1533 # For example, kernel uprev from 3.x to 4.x, they are two separate packages
1534 # and conflict with each other. Other possible cases are package renaming or
1535 # refactoring. Let's recreate chroot to work around them.
1536 if 'Detected file collision' in stdout:
1537 # Using wildcard between words because the text wraps to the next line
1538 # depending on length of package name and each line is prefixed with
1539 # package name.
1540 # Using ".{,100}" instead of ".*" to prevent regex matching time explodes
1541 # exponentially. 100 is chosen arbitrarily. It should be longer than any
1542 # package name (65 now).
1543 m = re.search(
1544 r'Package (\S+).{,100}NOT.{,100}merged.{,100}'
1545 r'due.{,100}to.{,100}file.{,100}collisions', stdout, re.S)
1546 if m:
1547 return 'failed to install package due to file collision: ' + m.group(1)
Kuang-che Wu356c3522019-11-19 16:11:05 +08001548
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001549 return None
1550
1551
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001552def build_packages(chromeos_root,
1553 board,
1554 chrome_root=None,
1555 goma_dir=None,
1556 afdo_use=False):
Kuang-che Wu28980b22019-07-31 19:51:45 +08001557 """Build ChromeOS packages.
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001558
1559 Args:
1560 chromeos_root: chromeos tree root
1561 board: ChromeOS board name
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07001562 chrome_root: Chrome tree root. If specified, build chrome using the provided
1563 tree
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001564 goma_dir: Goma installed directory to mount into the chroot. If specified,
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07001565 build chrome with goma.
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001566 afdo_use: build chrome with AFDO optimization
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001567 """
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001568
1569 def has_build_package_argument(argument):
1570 stderr_lines = []
1571 try:
1572 util.check_call(
1573 'src/scripts/build_packages',
1574 '--help',
1575 cwd=chromeos_root,
1576 stderr_callback=stderr_lines.append)
1577 except subprocess.CalledProcessError:
1578 help_output = ''.join(stderr_lines)
1579 return '--[no]%s' % argument in help_output
1580
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001581 common_env = {
1582 'USE': '-cros-debug chrome_internal',
1583 'FEATURES': 'separatedebug',
1584 }
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001585 stderr_lines = []
1586 try:
Kuang-che Wufb553102018-10-02 18:14:29 +08001587 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001588 env = common_env.copy()
1589 env['FEATURES'] += ' -separatedebug splitdebug'
Kuang-che Wufb553102018-10-02 18:14:29 +08001590 cros_sdk(
1591 chromeos_root,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001592 './update_chroot',
1593 '--toolchain_boards',
Kuang-che Wufb553102018-10-02 18:14:29 +08001594 board,
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001595 env=env,
Kuang-che Wu28980b22019-07-31 19:51:45 +08001596 stderr_callback=stderr_lines.append)
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001597
1598 env = common_env.copy()
1599 cmd = [
Kuang-che Wu28980b22019-07-31 19:51:45 +08001600 './build_packages',
1601 '--board',
1602 board,
1603 '--withdev',
1604 '--noworkon',
1605 '--skip_chroot_upgrade',
1606 '--accept_licenses=@CHROMEOS',
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001607 ]
Zheng-Jie Changffd49462019-12-16 12:15:18 +08001608
1609 # `use_any_chrome` flag is default on and will force to use a chrome
1610 # prebuilt even if the version doesn't match.
1611
1612 # As this argument is landed in 12681, we should check if the argument
1613 # exists before adding this.
1614 if has_build_package_argument('use_any_chrome'):
1615 cmd.append('--nouse_any_chrome')
1616
Kuang-che Wua9a20bb2019-09-05 22:24:04 +08001617 if goma_dir:
1618 # Tell build_packages to start and stop goma
1619 cmd.append('--run_goma')
1620 env['USE_GOMA'] = 'true'
1621 if afdo_use:
1622 env['USE'] += ' afdo_use'
1623 cros_sdk(
1624 chromeos_root,
1625 *cmd,
1626 env=env,
1627 chrome_root=chrome_root,
1628 stderr_callback=stderr_lines.append,
1629 goma_dir=goma_dir)
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001630 except subprocess.CalledProcessError as e:
1631 # Detect failures due to incompatibility between chroot and source tree. If
1632 # so, notify the caller to recreate chroot and retry.
1633 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1634 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001635 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu9890ce82018-07-07 15:14:10 +08001636
1637 # For other failures, don't know how to handle. Just bail out.
1638 raise
1639
Kuang-che Wu28980b22019-07-31 19:51:45 +08001640
1641def build_image(chromeos_root, board):
1642 """Build ChromeOS image.
1643
1644 Args:
1645 chromeos_root: chromeos tree root
1646 board: ChromeOS board name
1647
1648 Returns:
1649 image folder; relative to chromeos_root
1650 """
1651 stderr_lines = []
1652 try:
1653 with locking.lock_file(locking.LOCK_FILE_FOR_BUILD):
1654 cros_sdk(
1655 chromeos_root,
1656 './build_image',
1657 '--board',
1658 board,
1659 '--noenable_rootfs_verification',
1660 'test',
1661 env={
1662 'USE': '-cros-debug chrome_internal',
1663 'FEATURES': 'separatedebug',
1664 },
1665 stderr_callback=stderr_lines.append)
1666 except subprocess.CalledProcessError as e:
1667 # Detect failures due to incompatibility between chroot and source tree. If
1668 # so, notify the caller to recreate chroot and retry.
1669 reason = check_if_need_recreate_chroot(e.output, ''.join(stderr_lines))
1670 if reason:
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08001671 raise NeedRecreateChrootException(reason) from e
Kuang-che Wu28980b22019-07-31 19:51:45 +08001672
1673 # For other failures, don't know how to handle. Just bail out.
1674 raise
1675
Kuang-che Wu721e8902021-03-19 12:18:53 +08001676 image_symlink = os.path.join(chromeos_root, build_images_dir, board, 'latest')
Kuang-che Wu28980b22019-07-31 19:51:45 +08001677 assert os.path.exists(image_symlink)
1678 image_name = os.readlink(image_symlink)
Kuang-che Wu721e8902021-03-19 12:18:53 +08001679 image_folder = os.path.join(build_images_dir, board, image_name)
Kuang-che Wu28980b22019-07-31 19:51:45 +08001680 assert os.path.exists(
1681 os.path.join(chromeos_root, image_folder, test_image_filename))
1682 return image_folder
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001683
1684
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07001685def workaround_b183567529(host,
1686 board,
1687 version=None,
Zheng-Jie Changfc5d8742021-03-31 04:57:43 +08001688 force_reboot_callback=None):
1689 """Workaround for volteer failure.
1690
1691 See b/183567529#comment8 and b/183020319#comment26 for more details.
1692 """
1693 broken_range = [
1694 ('13836.0.0', '13854.0.0'),
1695 ('13816.13.0', '13816.19.0'),
1696 ]
1697 if board != 'volteer' or not version:
1698 return
1699
1700 if is_cros_short_version(version):
1701 short_version = version
1702 else:
1703 _, short_version = version_split(version)
1704 for old, new in broken_range:
1705 if util.is_version_lesseq(old, short_version) and util.is_version_lesseq(
1706 short_version,
1707 new) and (util.is_direct_relative_version(old, short_version) and
1708 util.is_direct_relative_version(short_version, new)):
1709 logger.info('applying b183567529 cbi patch for volteer')
1710 cbi_override = os.path.join(
1711 common.BISECT_KIT_ROOT,
1712 'patching/b183567529/volteer-cbi-override.conf')
1713 util.scp_cmd(cbi_override, 'root@%s:/etc/init/' % host)
1714
1715 patch_script = os.path.join(common.BISECT_KIT_ROOT,
1716 'patching/b183567529/test_cbi_script.sh')
1717 util.check_output(patch_script, host)
1718 reboot(host, force_reboot_callback)
1719 break
1720
1721
Kuang-che Wu23192ad2020-03-11 18:12:46 +08001722class AutotestControlInfo:
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001723 """Parsed content of autotest control file.
1724
1725 Attributes:
1726 name: test name
1727 path: control file path
1728 variables: dict of top-level control variables. Sample keys: NAME, AUTHOR,
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07001729 DOC, ATTRIBUTES, DEPENDENCIES, etc.
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001730 """
1731
1732 def __init__(self, path, variables):
Kuang-che Wu25fec6f2021-01-28 12:40:43 +08001733 assert 'NAME' in variables, 'invalid control file'
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001734 self.name = variables['NAME']
1735 self.path = path
1736 self.variables = variables
1737
1738
1739def parse_autotest_control_file(path):
1740 """Parses autotest control file.
1741
1742 This only parses simple top-level string assignments.
1743
1744 Returns:
1745 AutotestControlInfo object
1746 """
1747 variables = {}
Kuang-che Wua5723492019-11-25 20:59:34 +08001748 with open(path) as f:
1749 code = ast.parse(f.read())
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001750 for stmt in code.body:
1751 # Skip if not simple "NAME = *" assignment.
1752 if not (isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and
1753 isinstance(stmt.targets[0], ast.Name)):
1754 continue
1755
1756 # Only support string value.
1757 if isinstance(stmt.value, ast.Str):
1758 variables[stmt.targets[0].id] = stmt.value.s
1759
1760 return AutotestControlInfo(path, variables)
1761
1762
1763def enumerate_autotest_control_files(autotest_dir):
1764 """Enumerate autotest control files.
1765
1766 Args:
1767 autotest_dir: autotest folder
1768
1769 Returns:
1770 list of paths to control files
1771 """
1772 # Where to find control files. Relative to autotest_dir.
1773 subpaths = [
1774 'server/site_tests',
1775 'client/site_tests',
1776 'server/tests',
1777 'client/tests',
1778 ]
1779
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001780 denylist = ['site-packages', 'venv', 'results', 'logs', 'containers']
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001781 result = []
1782 for subpath in subpaths:
1783 path = os.path.join(autotest_dir, subpath)
1784 for root, dirs, files in os.walk(path):
1785
Kuang-che Wud6df63c2020-09-20 01:29:55 +08001786 for deny in denylist:
1787 if deny in dirs:
1788 dirs.remove(deny)
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001789
1790 for filename in files:
1791 if filename == 'control' or filename.startswith('control.'):
1792 result.append(os.path.join(root, filename))
1793
1794 return result
1795
1796
1797def get_autotest_test_info(autotest_dir, test_name):
1798 """Get metadata of given test.
1799
1800 Args:
1801 autotest_dir: autotest folder
1802 test_name: test name
1803
1804 Returns:
1805 AutotestControlInfo object. None if test not found.
1806 """
1807 for control_file in enumerate_autotest_control_files(autotest_dir):
Zheng-Jie Chang1504a552020-02-20 16:38:57 +08001808 try:
1809 info = parse_autotest_control_file(control_file)
1810 except SyntaxError:
1811 logger.warning('%s is not parsable, ignore', control_file)
1812 continue
1813
Kuang-che Wub9705bd2018-06-28 17:59:18 +08001814 if info.name == test_name:
1815 return info
1816 return None
1817
1818
Kuang-che Wu9d14c162020-11-03 19:35:18 +08001819def _get_overlay_name(overlay):
1820 path = os.path.join(overlay, 'metadata', 'layout.conf')
1821 if os.path.exists(path):
1822 with open(path) as f:
1823 for line in f:
1824 m = re.search(r'repo-name\s*=\s*(\S+)', line)
1825 if m:
1826 return m.group(1)
1827
1828 path = os.path.join(overlay, 'profiles', 'repo_name')
1829 if os.path.exists(path):
1830 with open(path) as f:
1831 return f.readline().rstrip()
1832
1833 return None
1834
1835
1836def parse_chromeos_overlays(chromeos_root):
1837 # ref: chromite's lib/portage_util.py ListOverlays().
1838 overlays = {}
1839 paths = ['src/overlays', 'src/private-overlays']
1840
1841 for path in paths:
1842 path = os.path.join(chromeos_root, path, 'overlay-*')
1843 for overlay in sorted(glob.glob(path)):
1844 name = _get_overlay_name(overlay)
1845 if not name:
1846 continue
1847 # Special cases which have variant boards.
1848 if name in ['auron', 'guado', 'nyan', 'veyron']:
1849 continue
1850
1851 path = os.path.join(overlay, 'metadata', 'layout.conf')
1852 masters = []
1853 if os.path.exists(path):
1854 with open(path) as f:
1855 for line in f:
1856 m = re.search(r'masters\s*=(.*)', line)
1857 if m:
1858 masters = m.group(1).split()
1859 overlays[name] = masters
1860 return overlays
1861
1862
1863def resolve_basic_boards(overlays):
1864
1865 def normalize(name):
1866 return name.replace('-private', '')
1867
1868 def resolve(name):
1869 result = set()
1870 for parent in overlays[name]:
1871 assert parent != name, 'recursive overlays definition?'
1872 if parent not in overlays:
1873 continue
1874 for basic in resolve(parent):
1875 result.add(basic)
1876 if not result:
1877 result.add(name)
1878 return set(map(normalize, result))
1879
1880 result = {}
1881 for name in overlays:
1882 board = normalize(name)
1883 basic = resolve(name)
1884 assert len(basic) == 1
1885 basic_board = basic.pop()
1886 result[board] = basic_board
1887 return result
1888
1889
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08001890def detect_branch_level(branch):
1891 """Given a branch name of manifest-internal, detect it's branch level.
1892
1893 level1: if ChromeOS version is x.0.0
1894 level2: if ChromeOS version is x.x.0
1895 level3: if ChromeOS version is x.x.x
1896 Where x is an non-zero integer.
1897
1898 Args:
1899 branch: branch name or ref name in manifest-internal
1900
1901 Returns:
1902 An integer indicates the branch level, or zero if not detectable.
1903 """
1904 level1 = r'^(refs\/\S+(\/\S+)?/)?master$'
1905 level2 = r'^\S+-(\d+)(\.0)?\.B$'
1906 level3 = r'^\S+-(\d+)\.(\d+)(\.0)?\.B$'
1907
1908 if re.match(level1, branch):
1909 return 1
1910 if re.match(level2, branch):
1911 return 2
1912 if re.match(level3, branch):
1913 return 3
1914 return 0
1915
1916
Zheng-Jie Chang5f9ae4e2020-02-07 14:26:06 +08001917def get_crosland_link(old, new):
1918 """Generates crosland link between two versions.
1919
1920 Args:
1921 old: ChromeOS version
1922 new: ChromeOS version
1923
1924 Returns:
1925 A crosland url.
1926 """
1927
1928 def version_to_url_parameter(ver):
1929 if is_cros_snapshot_version(ver):
1930 return snapshot_version_split(ver)[2]
1931 return version_to_short(ver)
1932
1933 old_parameter = version_to_url_parameter(old)
1934 new_parameter = version_to_url_parameter(new)
1935 return CROSLAND_URL_TEMPLATE % (old_parameter, new_parameter)
1936
1937
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001938class ChromeOSSpecManager(codechange.SpecManager):
1939 """Repo manifest related operations.
1940
1941 This class enumerates chromeos manifest files, parses them,
1942 and sync to disk state according to them.
1943 """
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001944
1945 def __init__(self, config):
1946 self.config = config
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001947 self.manifest_dir = os.path.join(self.config['chromeos_root'], '.repo',
1948 'manifests')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001949 self.manifest_internal_dir = os.path.join(self.config['chromeos_mirror'],
1950 'manifest-internal.git')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001951 self.historical_manifest_git_dir = os.path.join(
Kuang-che Wud8fc9572018-10-03 21:00:41 +08001952 self.config['chromeos_mirror'], 'chromeos/manifest-versions.git')
Kuang-che Wu0fff9882020-12-14 17:37:31 +08001953 self.historical_manifest_branch_name = 'refs/heads/master'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001954 if not os.path.exists(self.historical_manifest_git_dir):
Kuang-che Wue121fae2018-11-09 16:18:39 +08001955 raise errors.InternalError('Manifest snapshots should be cloned into %s' %
1956 self.historical_manifest_git_dir)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08001957
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001958 def lookup_snapshot_manifest_revisions(self, old, new):
1959 """Get manifest commits between snapshot versions.
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08001960
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001961 Returns:
1962 list of (timestamp, commit_id, snapshot_id):
1963 timestamp: integer unix timestamp
1964 commit_id: a string indicates commit hash
1965 snapshot_id: a string indicates snapshot id
1966 """
1967 assert is_cros_snapshot_version(old)
1968 assert is_cros_snapshot_version(new)
1969
1970 gs_path = (
Zheng-Jie Chang54020832020-04-21 15:52:48 +08001971 'gs://chromeos-image-archive/{board}-snapshot/{version}-*/image.zip')
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001972 # Try to guess the commit time of a snapshot manifest, it is usually a few
1973 # minutes different between snapshot manifest commit and image.zip
1974 # generate.
1975 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001976 old_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001977 gs_path.format(board=self.config['board'], version=old)) - 86400
1978 except subprocess.CalledProcessError:
1979 old_timestamp = None
1980 try:
Kuang-che Wu876a5382020-10-27 14:24:58 +08001981 new_timestamp = gsutil_stat_creation_time(
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08001982 gs_path.format(board=self.config['board'], version=new)) + 86400
1983 # 1558657989 is snapshot_id 5982's commit time, this ensures every time
1984 # we can find snapshot 5982
1985 # snapshot_id <= 5982 has different commit message format, so we need
1986 # to identify its id in different ways, see below comment for more info.
1987 new_timestamp = max(new_timestamp, 1558657989 + 1)
1988 except subprocess.CalledProcessError:
1989 new_timestamp = None
1990 result = []
1991 _, _, old_snapshot_id = snapshot_version_split(old)
1992 _, _, new_snapshot_id = snapshot_version_split(new)
1993 repo = self.manifest_internal_dir
1994 path = 'snapshot.xml'
1995 branch = 'snapshot'
1996 commits = git_util.get_history(
1997 repo,
1998 path,
1999 branch,
2000 after=old_timestamp,
2001 before=new_timestamp,
2002 with_subject=True)
2003
2004 # Unfortunately, we can not identify snapshot_id <= 5982 from its commit
2005 # subject, as their subjects are all `Annealing manifest snapshot.`.
2006 # So instead we count the snapshot_id manually.
2007 count = 5982
2008 # There are two snapshot_id = 2633 in commit history, ignore the former
2009 # one.
2010 ignore_list = ['95c8526a7f0798d02f692010669dcbd5a152439a']
2011 # We examine the commits in reverse order as there are some testing
2012 # commits before snapshot_id=2, this method works fine after
2013 # snapshot 2, except snapshot 2633
2014 for commit in reversed(commits):
2015 msg = commit[2]
2016 if commit[1] in ignore_list:
2017 continue
2018
2019 match = re.match(r'^annealing manifest snapshot (\d+)', msg)
2020 if match:
2021 snapshot_id = match.group(1)
2022 elif 'Annealing manifest snapshot' in msg:
2023 snapshot_id = str(count)
2024 count -= 1
2025 else:
2026 continue
Zheng-Jie Chang34595ea2020-03-10 13:04:22 +08002027 # b/151054108: snapshot version in [29288, 29439] is broken
2028 if 29288 <= int(snapshot_id) <= 29439:
2029 continue
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002030 if int(old_snapshot_id) <= int(snapshot_id) <= int(new_snapshot_id):
2031 result.append((commit[0], commit[1], snapshot_id))
2032 # We find commits in reversed order, now reverse it again to chronological
2033 # order.
2034 return list(reversed(result))
2035
2036 def lookup_build_timestamp(self, rev):
2037 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
2038 if is_cros_full_version(rev):
2039 return self.lookup_release_build_timestamp(rev)
Kuang-che Wua7ddf9b2019-11-25 18:59:57 +08002040 return self.lookup_snapshot_build_timestamp(rev)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002041
2042 def lookup_snapshot_build_timestamp(self, rev):
2043 assert is_cros_snapshot_version(rev)
2044 return int(self.lookup_snapshot_manifest_revisions(rev, rev)[0][0])
2045
2046 def lookup_release_build_timestamp(self, rev):
2047 assert is_cros_full_version(rev)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002048 milestone, short_version = version_split(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002049 path = os.path.join('buildspecs', milestone, short_version + '.xml')
2050 try:
2051 timestamp = git_util.get_commit_time(self.historical_manifest_git_dir,
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002052 self.historical_manifest_branch_name,
2053 path)
Kuang-che Wu6d91b8c2020-11-24 20:14:35 +08002054 except ValueError as e:
2055 raise errors.InternalError(
2056 '%s does not have %s' %
2057 (self.historical_manifest_git_dir, path)) from e
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002058 return timestamp
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002059
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08002060 def detect_float_spec_branch_level(self, spec):
2061 results = [
2062 detect_branch_level(branch) for branch in git_util.get_branches(
2063 self.manifest_dir, commit=spec.name)
2064 ]
2065 results = [x for x in results if x > 0]
2066 return min(results) if results else 0
2067
2068 def branch_between_float_specs(self, old_spec, new_spec):
2069 if old_spec.spec_type != codechange.SPEC_FLOAT:
2070 return False
2071 if new_spec.spec_type != codechange.SPEC_FLOAT:
2072 return False
2073
2074 level_old = self.detect_float_spec_branch_level(old_spec)
2075 level_new = self.detect_float_spec_branch_level(new_spec)
2076
2077 if not level_old or not level_new:
Kuang-che Wuebc2c362020-12-14 16:27:09 +08002078 logger.warning('branch level detect failed, assume not branched')
Zheng-Jie Chang868c1752020-01-21 14:42:41 +08002079 return False
2080 return level_old != level_new
2081
Kuang-che Wud558a042020-06-06 02:11:00 +08002082 def _determine_float_branch(self, old, new, fixed_specs):
2083 # There is no revision tag in snapshot's xml. We know snapshot
2084 # builds are on master branch.
2085 master_refname = 'refs/remotes/origin/master'
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08002086 if fixed_specs[0].revision:
2087 old_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08002088 self.manifest_dir, commit=fixed_specs[0].revision, remote=True)
2089 else:
2090 old_branches = [master_refname]
2091
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08002092 if fixed_specs[-1].revision:
2093 new_branches = git_util.get_branches(
Kuang-che Wud558a042020-06-06 02:11:00 +08002094 self.manifest_dir, commit=fixed_specs[-1].revision, remote=True)
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08002095 else:
Kuang-che Wud558a042020-06-06 02:11:00 +08002096 new_branches = [master_refname]
Zheng-Jie Chang3e191962020-02-06 14:25:31 +08002097
Kuang-che Wud558a042020-06-06 02:11:00 +08002098 common_branches = list(set(old_branches) & set(new_branches))
2099 assert common_branches, '%s and %s are not on common branches?' % (old, new)
2100
2101 if len(common_branches) == 1:
2102 return common_branches[0]
2103
2104 # There are more than one common branches, use heuristic to tie breaking.
2105 # The heuristic is simple: choice the branch with "smallest" number.
2106 # "Smaller" means the more major branch (not branched) or branched later.
2107 #
2108 # Following is the commit graph of manifest-internal repo. It shows many
2109 # interesting cases.
2110 #
2111 # 84/13021.0.0 84/13022.0.0 84/13024.0.0
2112 # --A--+---X--------------X------B-------X-----------> master
2113 # \
2114 # \ 83/13020.1.0 83/13020.56.0 83/13020.68.0
2115 # C---X----D--+-------X-------+--------X-----> release-R83-13020.B
2116 # \ \
2117 # \ E------------> stabilize-13020.67.B
2118 # \ 83/13020.55.1
2119 # F-----X--------------------> stabilize-13020.55.B
2120 #
2121 # How to read this graph:
2122 # - Time goes from left to right. Branch names are on the right side of
2123 # arrows.
2124 # - Letters A-F are manifest commits.
2125 # - Marker X means release image build at that time, the version numbers
2126 # are labeled above the X marker.
2127 # For example,
2128 # 1) 13021.0.0 release is based on manifest A, which is on all branches
2129 # shown on the graph.
2130 # We know 13021.0.0 is on master (and R84 branch later, not shown in
2131 # this graph), not on 13020* branches.
2132 # 2) 13020.56.0 release is based on manifest D, which is on 3 branches
2133 # (R83-13020.B, 13020.67.B, and 13020.55.B).
2134 # We know 13020.56.0 is on R83-13020.B and 13020.67.B, but not
2135 # 13020.55.B.
2136 #
2137 # There is an important property here. Every time a new branch is created,
2138 # there will always be a commit (like C, E, and F) to fix "revision" field
2139 # in the manifest file. In other words, xxxxx.1.0 is impossible based on
2140 # manifest on master branch. xxxxx.yy.1 is impossible based on manifest on
2141 # xxxxx.B branch.
2142 #
2143 # With such property, among the branches containing the given manifest
2144 # file, the branch with "smallest" number guarantees where the release is.
2145
2146 def branch_key(s):
2147 if s == master_refname:
2148 return 0, 0, 0
2149 m = re.search(r'-(\d+)\.B$', s)
2150 if m:
2151 return int(m.group(1)), 0, 0
2152 m = re.search(r'-(\d+)\.(\d+)\.B$', s)
2153 if m:
2154 return int(m.group(1)), int(m.group(2)), 0
2155 m = re.search(r'-(\d+)\.(\d+)\.(\d+)\.B$', s)
2156 if m:
2157 return int(m.group(1)), int(m.group(2)), int(m.group(3))
2158
2159 logger.warning('unexpected branch name: %s', s)
2160 return (sys.maxsize, sys.maxsize, sys.maxsize, s)
2161
2162 common_branches.sort(key=branch_key)
2163 return common_branches[0]
2164
2165 def collect_float_spec(self, old, new, fixed_specs=None):
2166 assert fixed_specs
2167 branch = self._determine_float_branch(old, new, fixed_specs)
2168 logger.debug('float branch=%s', branch)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002169
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002170 old_timestamp = self.lookup_build_timestamp(old)
2171 new_timestamp = self.lookup_build_timestamp(new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002172 # snapshot time is different from commit time
2173 # usually it's a few minutes different
2174 # 30 minutes should be safe in most cases
2175 if is_cros_snapshot_version(old):
2176 old_timestamp = old_timestamp - 1800
2177 if is_cros_snapshot_version(new):
2178 new_timestamp = new_timestamp + 1800
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002179
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08002180 # TODO(zjchang): add logic to combine symlink target's (full.xml) history
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002181 result = []
Zheng-Jie Chang1ace3012020-02-15 04:51:05 +08002182 path = 'default.xml'
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002183 parser = repo_util.ManifestParser(self.manifest_dir)
2184 for timestamp, git_rev in parser.enumerate_manifest_commits(
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002185 old_timestamp, new_timestamp, path, branch=branch):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002186 result.append(
2187 codechange.Spec(codechange.SPEC_FLOAT, git_rev, timestamp, path))
2188 return result
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002189
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002190 def collect_fixed_spec(self, old, new):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002191 assert is_cros_full_version(old) or is_cros_snapshot_version(old)
2192 assert is_cros_full_version(new) or is_cros_snapshot_version(new)
2193
2194 # case 1: if both are snapshot, return a list of snapshot
2195 if is_cros_snapshot_version(old) and is_cros_snapshot_version(new):
2196 return self.collect_snapshot_specs(old, new)
2197
2198 # case 2: if both are release version
2199 # return a list of release version
2200 if is_cros_full_version(old) and is_cros_full_version(new):
2201 return self.collect_release_specs(old, new)
2202
2203 # case 3: return a list of release version and append a snapshot
2204 # before or at the end
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002205 result = self.collect_release_specs(
2206 version_to_full(self.config['board'], old),
2207 version_to_full(self.config['board'], new))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002208 if is_cros_snapshot_version(old):
Zheng-Jie Changc47af3a2019-11-11 17:28:58 +08002209 result = self.collect_snapshot_specs(old, old) + result[1:]
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002210 elif is_cros_snapshot_version(new):
Zheng-Jie Chang5cd62dd2019-12-09 13:21:12 +08002211 result += self.collect_snapshot_specs(new, new)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002212 return result
2213
2214 def collect_snapshot_specs(self, old, new):
2215 assert is_cros_snapshot_version(old)
2216 assert is_cros_snapshot_version(new)
2217
2218 def guess_snapshot_version(board, snapshot_id, old, new):
2219 if old.endswith('-' + snapshot_id):
2220 return old
2221 if new.endswith('-' + snapshot_id):
2222 return new
Zheng-Jie Chang54020832020-04-21 15:52:48 +08002223 gs_path = ('gs://chromeos-image-archive/{board}-snapshot/'
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002224 'R*-{snapshot_id}-*'.format(
2225 board=board, snapshot_id=snapshot_id))
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002226 for line in gsutil_ls(gs_path, ignore_errors=True):
2227 m = re.match(r'^gs:\S+(R\d+-\d+\.\d+\.\d+-\d+)\S+', line)
2228 if m:
2229 return m.group(1)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002230 return None
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002231
2232 result = []
2233 path = 'snapshot.xml'
2234 revisions = self.lookup_snapshot_manifest_revisions(old, new)
Kuang-che Wuf791afa2019-10-28 19:53:26 +08002235 for timestamp, _git_rev, snapshot_id in revisions:
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002236 snapshot_version = guess_snapshot_version(self.config['board'],
2237 snapshot_id, old, new)
Zheng-Jie Chang026cd5d2019-12-04 12:13:01 +08002238 if snapshot_version:
2239 result.append(
2240 codechange.Spec(codechange.SPEC_FIXED, snapshot_version, timestamp,
2241 path))
2242 else:
2243 logger.warning('snapshot id %s is not found, ignore', snapshot_id)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002244 return result
2245
2246 def collect_release_specs(self, old, new):
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002247 assert is_cros_full_version(old)
2248 assert is_cros_full_version(new)
2249 old_milestone, old_short_version = version_split(old)
2250 new_milestone, new_short_version = version_split(new)
2251
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002252 result = []
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002253 for milestone in git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002254 self.historical_manifest_git_dir, self.historical_manifest_branch_name,
2255 'buildspecs'):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002256 if not milestone.isdigit():
2257 continue
2258 if not int(old_milestone) <= int(milestone) <= int(new_milestone):
2259 continue
2260
Kuang-che Wu74768d32018-09-07 12:03:24 +08002261 files = git_util.list_dir_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002262 self.historical_manifest_git_dir,
2263 self.historical_manifest_branch_name,
Kuang-che Wu74768d32018-09-07 12:03:24 +08002264 os.path.join('buildspecs', milestone))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002265
2266 for fn in files:
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002267 path = os.path.join('buildspecs', milestone, fn)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002268 short_version, ext = os.path.splitext(fn)
2269 if ext != '.xml':
2270 continue
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002271 if (util.is_version_lesseq(old_short_version, short_version) and
2272 util.is_version_lesseq(short_version, new_short_version) and
2273 util.is_direct_relative_version(short_version, new_short_version)):
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002274 rev = make_cros_full_version(milestone, short_version)
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002275 timestamp = git_util.get_commit_time(
2276 self.historical_manifest_git_dir,
2277 self.historical_manifest_branch_name, path)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002278 result.append(
2279 codechange.Spec(codechange.SPEC_FIXED, rev, timestamp, path))
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002280
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002281 def version_key_func(spec):
2282 _milestone, short_version = version_split(spec.name)
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002283 return util.version_key_func(short_version)
2284
2285 result.sort(key=version_key_func)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002286 assert result[0].name == old
2287 assert result[-1].name == new
Kuang-che Wubfc4a642018-04-19 11:54:08 +08002288 return result
2289
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002290 def get_manifest(self, rev):
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002291 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
2292 if is_cros_full_version(rev):
2293 milestone, short_version = version_split(rev)
2294 path = os.path.join('buildspecs', milestone, '%s.xml' % short_version)
2295 manifest = git_util.get_file_from_revision(
Kuang-che Wu0fff9882020-12-14 17:37:31 +08002296 self.historical_manifest_git_dir,
2297 self.historical_manifest_branch_name, path)
Zheng-Jie Chang127c3302019-09-10 17:17:04 +08002298 else:
2299 revisions = self.lookup_snapshot_manifest_revisions(rev, rev)
2300 commit_id = revisions[0][1]
2301 manifest = git_util.get_file_from_revision(self.manifest_internal_dir,
2302 commit_id, 'snapshot.xml')
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002303 return manifest
2304
2305 def get_manifest_file(self, rev):
2306 assert is_cros_full_version(rev) or is_cros_snapshot_version(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002307 manifest_name = 'manifest_%s.xml' % rev
2308 manifest_path = os.path.join(self.manifest_dir, manifest_name)
2309 with open(manifest_path, 'w') as f:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002310 f.write(self.get_manifest(rev))
Zheng-Jie Changec368b52020-03-02 16:25:25 +08002311
2312 # workaround for b/150572399
2313 # for chromeOS version < 12931.0.0, manifests are included from incorrect
2314 # folder .repo instead of.repo/manifests
2315 if is_cros_version_lesseq(rev, '12931.0.0'):
2316 repo_path = os.path.join(self.config['chromeos_root'], '.repo')
2317 manifest_patch_path = os.path.join(repo_path, manifest_name)
2318 with open(manifest_patch_path, 'w') as f:
2319 f.write(self.get_manifest(rev))
2320
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002321 return manifest_name
2322
2323 def parse_spec(self, spec):
2324 parser = repo_util.ManifestParser(self.manifest_dir)
2325 if spec.spec_type == codechange.SPEC_FIXED:
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002326 manifest_name = self.get_manifest_file(spec.name)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002327 manifest_path = os.path.join(self.manifest_dir, manifest_name)
Kuang-che Wua5723492019-11-25 20:59:34 +08002328 with open(manifest_path) as f:
2329 content = f.read()
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002330 root = parser.parse_single_xml(content, allow_include=False)
2331 else:
2332 root = parser.parse_xml_recursive(spec.name, spec.path)
2333
2334 spec.entries = parser.process_parsed_result(root)
2335 if spec.spec_type == codechange.SPEC_FIXED:
Kuang-che Wufe1e88a2019-09-10 21:52:25 +08002336 if not spec.is_static():
Kuang-che Wud1b74152020-05-20 08:46:46 +08002337 raise ValueError('fixed spec %r has unexpected floating entries' %
2338 spec.name)
Zheng-Jie Changd968f552020-01-16 13:31:57 +08002339 spec.revision = root.get('revision')
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002340
2341 def sync_disk_state(self, rev):
Zheng-Jie Chang0fc704b2019-12-09 18:43:38 +08002342 manifest_name = self.get_manifest_file(rev)
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002343
2344 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
2345 # manifest. 'repo sync -m' is not enough
2346 repo_util.init(
2347 self.config['chromeos_root'],
2348 'https://chrome-internal.googlesource.com/chromeos/manifest-internal',
2349 manifest_name=manifest_name,
2350 repo_url='https://chromium.googlesource.com/external/repo.git',
Kuang-che Wud8fc9572018-10-03 21:00:41 +08002351 reference=self.config['chromeos_mirror'],
Zheng-Jie Chang85529ad2020-03-06 18:40:18 +08002352 # b/150753074: moblab is in non-default group and causes mark_as_stable
2353 # fail
Kuang-che Wu084eef22020-03-11 18:29:48 +08002354 groups='default,moblab,platform-linux',
Kuang-che Wue4bae0b2018-07-19 12:10:14 +08002355 )
2356
2357 # Note, don't sync with current_branch=True for chromeos. One of its
2358 # build steps (inside mark_as_stable) executes "git describe" which
2359 # needs git tag information.
2360 repo_util.sync(self.config['chromeos_root'])
Jae Hoon Kim7cbf64e2021-04-16 10:06:32 -07002361
2362
2363def repair_dut(chromeos_root, dut):
2364 """Repairs the DUT based off the host-info files.
2365
2366 Currently, the host-info file stores are under the stores folders.
2367 Only jetstream support exists at this time.
2368
2369 The host-info files can be fetched from AdminRepair tasks from stainless to
2370 make this a generic solution for all CrOS devices.
2371
2372 Args:
2373 chromeos_root: the chromeos tree
2374 dut: the CrOS DUT to repair
2375
2376 Returns:
2377 True on success, False otherwise
2378 """
2379 host_info_subdir = 'stores'
2380 repair_cmd = [
2381 os.path.join(chromeos_root_inside_chroot, in_tree_autotest_dir,
2382 'server/autoserv'), '-s', '--host-info-subdir',
2383 host_info_subdir, '-m', dut, '--lab', 'True', '--local-only-host-info',
2384 'True', '-R', '-r', 'results', '-p'
2385 ]
2386
2387 # Reuse results if it exists.
2388 autoserv_results = os.path.join(chromeos_root, 'src', 'scripts', 'results')
2389 if os.path.exists(autoserv_results):
2390 repair_cmd.append('--use-existing-results')
2391
2392 # Copy the stores.
2393 autoserv_results_stores = os.path.join(autoserv_results, host_info_subdir)
2394 if os.path.exists(autoserv_results_stores):
2395 shutil.rmtree(autoserv_results_stores)
2396 shutil.copytree(
2397 'autoserv-stores',
2398 autoserv_results_stores,
2399 symlinks=True,
2400 dirs_exist_ok=True)
2401
2402 # The USB in the servo might have flaked or SSH connections.
2403 try:
2404 retries = 0
2405 while True:
2406 # Repair the DUT.
2407 cros_sdk(chromeos_root, *repair_cmd)
2408 logger.info('Repair successful')
2409 return True
2410 except subprocess.CalledProcessError:
2411 retries += 1
2412 if retries == 3:
2413 logger.info('Repair failed')
2414 return False
2415 logger.info('Retrying repair')
2416 finally:
2417 # Cleanup the stores.
2418 if os.path.exists(autoserv_results_stores):
2419 shutil.rmtree(autoserv_results_stores)