blob: 5bfef1a41d60ef166bfab07d2829d2ee7aab7065 [file] [log] [blame]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""ChromeOS utility.
5
6Terminology used in this module.
7 short_version: ChromeOS version number without milestone, like "9876.0.0".
8 full_version: ChromeOS version number with milestone, like "R62-9876.0.0".
9 version: if not specified, it could be in short or full format.
10"""
11
12from __future__ import print_function
13import errno
14import json
15import logging
16import os
17import re
18import subprocess
19import time
20
21from bisect_kit import cli
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080022from bisect_kit import core
23from bisect_kit import util
24
25logger = logging.getLogger(__name__)
26
27re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
28re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
29
30gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
31gs_release_path = (
32 'gs://chromeos-releases/{channel}-channel/{board}/{short_version}')
33
34# Assume gsutil is in PATH.
35gsutil_bin = 'gsutil'
36
37VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
38VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
39VERSION_KEY_MILESTONE = 'milestone'
40VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080041VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080042VERSION_KEY_ANDROID_BRANCH = 'android_branch'
43
44
45def is_cros_short_version(s):
46 """Determines if `s` is chromeos short version"""
47 return bool(re.match(re_chromeos_short_version, s))
48
49
50def is_cros_full_version(s):
51 """Determines if `s` is chromeos full version"""
52 return bool(re.match(re_chromeos_full_version, s))
53
54
55def is_cros_version(s):
56 """Determines if `s` is chromeos version (either short or full)"""
57 return is_cros_short_version(s) or is_cros_full_version(s)
58
59
60def make_cros_full_version(milestone, short_version):
61 """Makes full_version from milestone and short_version"""
62 return 'R%s-%s' % (milestone, short_version)
63
64
65def version_split(full_version):
66 """Splits full_version into milestone and short_version"""
67 assert is_cros_full_version(full_version)
68 milestone, short_version = full_version.split('-')
69 return milestone[1:], short_version
70
71
72def argtype_cros_version(s):
73 if not is_cros_version(s):
74 msg = 'invalid cros version'
75 raise cli.ArgTypeError(msg, '9876.0.0 or R62-9876.0.0')
76 return s
77
78
79def query_dut_lsb_release(host):
80 """Query /etc/lsb-release of given DUT
81
82 Args:
83 host: the DUT address
84
85 Returns:
86 dict for keys and values of /etc/lsb-release. Return empty dict if failed.
87 """
88 try:
89 output = util.check_output('ssh', host, 'cat', '/etc/lsb-release')
90 except subprocess.CalledProcessError:
91 return {}
92 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
93
94
95def is_dut(host):
96 """Determines whether a host is a chromeos device
97
98 Args:
99 host: the DUT address
100
101 Returns:
102 True if the host is a chromeos device.
103 """
104 return query_dut_lsb_release(host).get('DEVICETYPE') in [
105 'CHROMEBASE',
106 'CHROMEBIT',
107 'CHROMEBOOK',
108 'CHROMEBOX',
109 'REFERENCE',
110 ]
111
112
113def query_dut_board(host):
114 """Query board name of a given DUT"""
115 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
116
117
118def query_dut_short_version(host):
119 """Query short version of a given DUT"""
120 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
121
122
123def query_dut_boot_id(host, connect_timeout=None):
124 """Query boot id
125
126 Args:
127 host: DUT address
128 connect_timeout: connection timeout
129
130 Returns:
131 boot uuid
132 """
133 cmd = ['ssh']
134 if connect_timeout:
135 cmd += ['-oConnectTimeout=%d' % connect_timeout]
136 cmd += [host, 'cat', '/proc/sys/kernel/random/boot_id']
137 return util.check_output(*cmd).strip()
138
139
140def reboot(host):
141 """Reboot a DUT and verify"""
142 logger.debug('reboot %s', host)
143 boot_id = query_dut_boot_id(host)
144
145 # Depends on timing, ssh may return failure due to broken pipe,
146 # so don't check ssh return code.
147 util.call('ssh', host, 'reboot')
Kuang-che Wu708310b2018-03-28 17:24:34 +0800148 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800149
Kuang-che Wu708310b2018-03-28 17:24:34 +0800150
151def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800152 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800153 # (dev screen short delay) or more (long delay).
154 time.sleep(15)
155 for _ in range(100):
156 try:
157 # During boot, DUT does not response and thus ssh may hang a while. So
158 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
159 # set tight limit because it's inside retry loop.
160 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
161 return
162 except subprocess.CalledProcessError:
163 logger.debug('reboot not ready? sleep wait 1 sec')
164 time.sleep(1)
165
166 raise core.ExecutionFatalError('reboot failed?')
167
168
169def gsutil(*args, **kwargs):
170 """gsutil command line wrapper
171
172 Args:
173 args: command line arguments passed to gsutil
174 kwargs:
175 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
176 but the path not found.
177
178 Returns:
179 stdout of gsutil
180
181 Raises:
182 core.ExecutionFatalError: gsutil failed to run
183 subprocess.CalledProcessError: command failed
184 """
185 stderr_lines = []
186 try:
187 return util.check_output(
188 gsutil_bin, *args, stderr_callback=stderr_lines.append)
189 except subprocess.CalledProcessError as e:
190 stderr = ''.join(stderr_lines)
191 if re.search(r'ServiceException:.* does not have .*access', stderr):
192 raise core.ExecutionFatalError(
193 'gsutil failed due to permission. ' +
194 'Run "%s config" and follow its instruction. ' % gsutil_bin +
195 'Fill any string if it asks for project-id')
196 if kwargs.get('ignore_errors'):
197 return ''
198 raise
199 except OSError as e:
200 if e.errno == errno.ENOENT:
201 raise core.ExecutionFatalError(
202 'Unable to run %s. gsutil is not installed or not in PATH?' %
203 gsutil_bin)
204 raise
205
206
207def gsutil_ls(*args, **kwargs):
208 """gsutil ls
209
210 Args:
211 args: arguments passed to 'gsutil ls'
212 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800213 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800214 exception, ex. path not found.
215
216 Returns:
217 list of 'gsutil ls' result. One element for one line of gsutil output.
218
219 Raises:
220 subprocess.CalledProcessError: gsutil failed, usually means path not found
221 """
222 return gsutil('ls', *args, **kwargs).splitlines()
223
224
225def query_milestone_by_version(board, short_version):
226 """Query milestone by ChromeOS version number
227
228 Args:
229 board: ChromeOS board name
230 short_version: ChromeOS version number in short format, ex. 9300.0.0
231
232 Returns:
233 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
234 None if failed.
235 """
236 path = gs_archive_path.format(board=board) + '/R*-' + short_version
237 for line in gsutil_ls('-d', path, ignore_errors=True):
238 m = re.search(r'/R(\d+)-', line)
239 if not m:
240 continue
241 return m.group(1)
242
243 for channel in ['canary', 'dev', 'beta', 'stable']:
244 path = gs_release_path.format(
245 channel=channel, board=board, short_version=short_version)
246 for line in gsutil_ls(path, ignore_errors=True):
247 m = re.search(r'\bR(\d+)-' + short_version, line)
248 if not m:
249 continue
250 return m.group(1)
251
252 logger.error('unable to query milestone of %s for %s', short_version, board)
253 return None
254
255
256def recognize_version(board, version):
257 """Recognize ChromeOS version
258
259 Args:
260 board: ChromeOS board name
261 version: ChromeOS version number in short or full format
262
263 Returns:
264 (milestone, version in short format)
265 """
266 if is_cros_short_version(version):
267 milestone = query_milestone_by_version(board, version)
268 short_version = version
269 else:
270 milestone, short_version = version_split(version)
271 return milestone, short_version
272
273
274def version_to_short(version):
275 """Convert ChromeOS version number to short format
276
277 Args:
278 version: ChromeOS version number in short or full format
279
280 Returns:
281 version number in short format
282 """
283 if is_cros_short_version(version):
284 return version
285 _, short_version = version_split(version)
286 return short_version
287
288
289def version_to_full(board, version):
290 """Convert ChromeOS version number to full format
291
292 Args:
293 board: ChromeOS board name
294 version: ChromeOS version number in short or full format
295
296 Returns:
297 version number in full format
298 """
299 if is_cros_full_version(version):
300 return version
301 milestone = query_milestone_by_version(board, version)
302 return make_cros_full_version(milestone, version)
303
304
305def cros_flash(chromeos_root,
306 host,
307 board,
308 version,
309 clobber_stateful=False,
310 disable_rootfs_verification=True):
311 """Flash a DUT with ChromeOS image of given version
312
313 This is implemented by 'cros flash' command line.
314 This supports more old versions than 'cros flash'.
315
316 Args:
317 chromeos_root: use 'cros flash' of which chromeos tree
318 host: DUT address
319 board: ChromeOS board name
320 version: ChromeOS version in short or full format
321 clobber_stateful: Clobber stateful partition when performing update
322 disable_rootfs_verification: Disable rootfs verification after update
323 is completed
324 """
325 logger.info('cros_flash %s %s %s', host, board, version)
326 assert is_cros_version(version)
327
328 # Reboot is necessary because sometimes previous 'cros flash' failed and
329 # entered a bad state.
330 reboot(host)
331
332 full_version = version_to_full(board, version)
333 short_version = version_to_short(full_version)
334
335 image_path = None
336 gs_path = gs_archive_path.format(board=board) + '/' + full_version
337 if gsutil_ls('-d', gs_path, ignore_errors=True):
338 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
339 board=board, full_version=full_version)
340 else:
341 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
342 if not os.path.exists(tmp_dir):
343 os.makedirs(tmp_dir)
344 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800345 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800346 # to fetch the image by ourselves
347 for channel in ['canary', 'dev', 'beta', 'stable']:
348 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
349 full_version=full_version, board=board)
350 gs_path = gs_release_path.format(
351 channel=channel, board=board, short_version=short_version)
352 gs_path += '/' + fn
353 if gsutil_ls(gs_path, ignore_errors=True):
354 # TODO(kcwu): delete tmp
355 gsutil('cp', gs_path, tmp_dir)
356 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
357 image_path = os.path.abspath(
358 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
359 break
360
361 assert image_path
362
363 args = ['--no-ping', host, image_path]
364 if clobber_stateful:
365 args.append('--clobber-stateful')
366 if disable_rootfs_verification:
367 args.append('--disable-rootfs-verification')
368
369 # TODO(kcwu): clear cache of cros flash
370 util.check_call('chromite/bin/cros', 'flash', *args, cwd=chromeos_root)
371
372 # In the past, cros flash may fail with returncode=0
373 # So let's have an extra check.
374 assert query_dut_short_version(host) == short_version
375
376
377def version_info(board, version):
378 """Query subcomponents version info of given version of ChromeOS
379
380 Args:
381 board: ChromeOS board name
382 version: ChromeOS version number in short or full format
383
384 Returns:
385 dict of component and version info, including (if available):
386 cros_short_version: ChromeOS version
387 cros_full_version: ChromeOS version
388 milestone: milestone of ChromeOS
389 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800390 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800391 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
392 """
393 info = {}
394 full_version = version_to_full(board, version)
395
396 # Some boards may have only partial-metadata.json but no metadata.json.
397 # e.g. caroline R60-9462.0.0
398 # Let's try both.
399 metadata = None
400 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
401 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
402 metadata_filename)
403 metadata = gsutil('cat', path, ignore_errors=True)
404 if metadata:
405 o = json.loads(metadata)
406 v = o['version']
407 board_metadata = o['board-metadata'][board]
408 info.update({
409 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
410 VERSION_KEY_CROS_FULL_VERSION: v['full'],
411 VERSION_KEY_MILESTONE: v['milestone'],
412 VERSION_KEY_CR_VERSION: v['chrome'],
413 })
414
415 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +0800416 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800417 if 'android-branch' in v: # this appears since R58-9317.0.0
418 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
419 elif 'android-container-branch' in board_metadata:
420 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
421 break
422 else:
423 logger.error('Failed to read metadata from gs://chromeos-image-archive')
424 logger.error(
425 'Note, so far no quick way to look up version info for too old builds')
426
427 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800428
429
430def query_chrome_version(board, version):
431 """Queries chrome version of chromeos build
432
433 Args:
434 board: ChromeOS board name
435 version: ChromeOS version number in short or full format
436
437 Returns:
438 Chrome version number
439 """
440 info = version_info(board, version)
441 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +0800442
443
444def query_android_build_id(board, rev):
445 info = version_info(board, rev)
446 rev = info['android_build_id']
447 return rev
448
449
450def query_android_branch(board, rev):
451 info = version_info(board, rev)
452 rev = info['android_branch']
453 return rev