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