blob: 1730f69058c88b0b49c9bbf7f2cd622dfa835fe3 [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'
41VERSION_KEY_ANDROID_BID = 'android_bid'
42VERSION_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')
148
149 # For dev-mode test image, the reboot time is roughtly at least 16 seconds
150 # (dev screen short delay) or more (long delay).
151 time.sleep(15)
152 for _ in range(100):
153 try:
154 # During boot, DUT does not response and thus ssh may hang a while. So
155 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
156 # set tight limit because it's inside retry loop.
157 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
158 return
159 except subprocess.CalledProcessError:
160 logger.debug('reboot not ready? sleep wait 1 sec')
161 time.sleep(1)
162
163 raise core.ExecutionFatalError('reboot failed?')
164
165
166def gsutil(*args, **kwargs):
167 """gsutil command line wrapper
168
169 Args:
170 args: command line arguments passed to gsutil
171 kwargs:
172 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
173 but the path not found.
174
175 Returns:
176 stdout of gsutil
177
178 Raises:
179 core.ExecutionFatalError: gsutil failed to run
180 subprocess.CalledProcessError: command failed
181 """
182 stderr_lines = []
183 try:
184 return util.check_output(
185 gsutil_bin, *args, stderr_callback=stderr_lines.append)
186 except subprocess.CalledProcessError as e:
187 stderr = ''.join(stderr_lines)
188 if re.search(r'ServiceException:.* does not have .*access', stderr):
189 raise core.ExecutionFatalError(
190 'gsutil failed due to permission. ' +
191 'Run "%s config" and follow its instruction. ' % gsutil_bin +
192 'Fill any string if it asks for project-id')
193 if kwargs.get('ignore_errors'):
194 return ''
195 raise
196 except OSError as e:
197 if e.errno == errno.ENOENT:
198 raise core.ExecutionFatalError(
199 'Unable to run %s. gsutil is not installed or not in PATH?' %
200 gsutil_bin)
201 raise
202
203
204def gsutil_ls(*args, **kwargs):
205 """gsutil ls
206
207 Args:
208 args: arguments passed to 'gsutil ls'
209 kwargs: extra parameters, where
210 ignore_errors: if true, return empty list instead of rasing
211 exception, ex. path not found.
212
213 Returns:
214 list of 'gsutil ls' result. One element for one line of gsutil output.
215
216 Raises:
217 subprocess.CalledProcessError: gsutil failed, usually means path not found
218 """
219 return gsutil('ls', *args, **kwargs).splitlines()
220
221
222def query_milestone_by_version(board, short_version):
223 """Query milestone by ChromeOS version number
224
225 Args:
226 board: ChromeOS board name
227 short_version: ChromeOS version number in short format, ex. 9300.0.0
228
229 Returns:
230 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
231 None if failed.
232 """
233 path = gs_archive_path.format(board=board) + '/R*-' + short_version
234 for line in gsutil_ls('-d', path, ignore_errors=True):
235 m = re.search(r'/R(\d+)-', line)
236 if not m:
237 continue
238 return m.group(1)
239
240 for channel in ['canary', 'dev', 'beta', 'stable']:
241 path = gs_release_path.format(
242 channel=channel, board=board, short_version=short_version)
243 for line in gsutil_ls(path, ignore_errors=True):
244 m = re.search(r'\bR(\d+)-' + short_version, line)
245 if not m:
246 continue
247 return m.group(1)
248
249 logger.error('unable to query milestone of %s for %s', short_version, board)
250 return None
251
252
253def recognize_version(board, version):
254 """Recognize ChromeOS version
255
256 Args:
257 board: ChromeOS board name
258 version: ChromeOS version number in short or full format
259
260 Returns:
261 (milestone, version in short format)
262 """
263 if is_cros_short_version(version):
264 milestone = query_milestone_by_version(board, version)
265 short_version = version
266 else:
267 milestone, short_version = version_split(version)
268 return milestone, short_version
269
270
271def version_to_short(version):
272 """Convert ChromeOS version number to short format
273
274 Args:
275 version: ChromeOS version number in short or full format
276
277 Returns:
278 version number in short format
279 """
280 if is_cros_short_version(version):
281 return version
282 _, short_version = version_split(version)
283 return short_version
284
285
286def version_to_full(board, version):
287 """Convert ChromeOS version number to full format
288
289 Args:
290 board: ChromeOS board name
291 version: ChromeOS version number in short or full format
292
293 Returns:
294 version number in full format
295 """
296 if is_cros_full_version(version):
297 return version
298 milestone = query_milestone_by_version(board, version)
299 return make_cros_full_version(milestone, version)
300
301
302def cros_flash(chromeos_root,
303 host,
304 board,
305 version,
306 clobber_stateful=False,
307 disable_rootfs_verification=True):
308 """Flash a DUT with ChromeOS image of given version
309
310 This is implemented by 'cros flash' command line.
311 This supports more old versions than 'cros flash'.
312
313 Args:
314 chromeos_root: use 'cros flash' of which chromeos tree
315 host: DUT address
316 board: ChromeOS board name
317 version: ChromeOS version in short or full format
318 clobber_stateful: Clobber stateful partition when performing update
319 disable_rootfs_verification: Disable rootfs verification after update
320 is completed
321 """
322 logger.info('cros_flash %s %s %s', host, board, version)
323 assert is_cros_version(version)
324
325 # Reboot is necessary because sometimes previous 'cros flash' failed and
326 # entered a bad state.
327 reboot(host)
328
329 full_version = version_to_full(board, version)
330 short_version = version_to_short(full_version)
331
332 image_path = None
333 gs_path = gs_archive_path.format(board=board) + '/' + full_version
334 if gsutil_ls('-d', gs_path, ignore_errors=True):
335 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
336 board=board, full_version=full_version)
337 else:
338 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
339 if not os.path.exists(tmp_dir):
340 os.makedirs(tmp_dir)
341 # gs://chromeos-releases may have more old images than
342 # gs://chromeos-image-archive, but 'cros flash' dones't support it. We have
343 # to fetch the image by ourselves
344 for channel in ['canary', 'dev', 'beta', 'stable']:
345 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
346 full_version=full_version, board=board)
347 gs_path = gs_release_path.format(
348 channel=channel, board=board, short_version=short_version)
349 gs_path += '/' + fn
350 if gsutil_ls(gs_path, ignore_errors=True):
351 # TODO(kcwu): delete tmp
352 gsutil('cp', gs_path, tmp_dir)
353 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
354 image_path = os.path.abspath(
355 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
356 break
357
358 assert image_path
359
360 args = ['--no-ping', host, image_path]
361 if clobber_stateful:
362 args.append('--clobber-stateful')
363 if disable_rootfs_verification:
364 args.append('--disable-rootfs-verification')
365
366 # TODO(kcwu): clear cache of cros flash
367 util.check_call('chromite/bin/cros', 'flash', *args, cwd=chromeos_root)
368
369 # In the past, cros flash may fail with returncode=0
370 # So let's have an extra check.
371 assert query_dut_short_version(host) == short_version
372
373
374def version_info(board, version):
375 """Query subcomponents version info of given version of ChromeOS
376
377 Args:
378 board: ChromeOS board name
379 version: ChromeOS version number in short or full format
380
381 Returns:
382 dict of component and version info, including (if available):
383 cros_short_version: ChromeOS version
384 cros_full_version: ChromeOS version
385 milestone: milestone of ChromeOS
386 cr_version: Chrome version
387 android_bid: Android build id
388 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
389 """
390 info = {}
391 full_version = version_to_full(board, version)
392
393 # Some boards may have only partial-metadata.json but no metadata.json.
394 # e.g. caroline R60-9462.0.0
395 # Let's try both.
396 metadata = None
397 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
398 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
399 metadata_filename)
400 metadata = gsutil('cat', path, ignore_errors=True)
401 if metadata:
402 o = json.loads(metadata)
403 v = o['version']
404 board_metadata = o['board-metadata'][board]
405 info.update({
406 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
407 VERSION_KEY_CROS_FULL_VERSION: v['full'],
408 VERSION_KEY_MILESTONE: v['milestone'],
409 VERSION_KEY_CR_VERSION: v['chrome'],
410 })
411
412 if 'android' in v:
413 info[VERSION_KEY_ANDROID_BID] = v['android']
414 if 'android-branch' in v: # this appears since R58-9317.0.0
415 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
416 elif 'android-container-branch' in board_metadata:
417 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
418 break
419 else:
420 logger.error('Failed to read metadata from gs://chromeos-image-archive')
421 logger.error(
422 'Note, so far no quick way to look up version info for too old builds')
423
424 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800425
426
427def query_chrome_version(board, version):
428 """Queries chrome version of chromeos build
429
430 Args:
431 board: ChromeOS board name
432 version: ChromeOS version number in short or full format
433
434 Returns:
435 Chrome version number
436 """
437 info = version_info(board, version)
438 return info['cr_version']