blob: 63f9ae42b4edd3699f6ab4506dd59568452340ef [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
Kuang-che Wubfc4a642018-04-19 11:54:08 +080023from bisect_kit import git_util
24from bisect_kit import repo_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080025from bisect_kit import util
26
27logger = logging.getLogger(__name__)
28
29re_chromeos_short_version = r'^\d+\.\d+\.\d+$'
30re_chromeos_full_version = r'^R\d+-\d+\.\d+\.\d+$'
31
32gs_archive_path = 'gs://chromeos-image-archive/{board}-release'
33gs_release_path = (
34 'gs://chromeos-releases/{channel}-channel/{board}/{short_version}')
35
36# Assume gsutil is in PATH.
37gsutil_bin = 'gsutil'
38
39VERSION_KEY_CROS_SHORT_VERSION = 'cros_short_version'
40VERSION_KEY_CROS_FULL_VERSION = 'cros_full_version'
41VERSION_KEY_MILESTONE = 'milestone'
42VERSION_KEY_CR_VERSION = 'cr_version'
Kuang-che Wu708310b2018-03-28 17:24:34 +080043VERSION_KEY_ANDROID_BUILD_ID = 'android_build_id'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080044VERSION_KEY_ANDROID_BRANCH = 'android_branch'
45
46
47def is_cros_short_version(s):
48 """Determines if `s` is chromeos short version"""
49 return bool(re.match(re_chromeos_short_version, s))
50
51
52def is_cros_full_version(s):
53 """Determines if `s` is chromeos full version"""
54 return bool(re.match(re_chromeos_full_version, s))
55
56
57def is_cros_version(s):
58 """Determines if `s` is chromeos version (either short or full)"""
59 return is_cros_short_version(s) or is_cros_full_version(s)
60
61
62def make_cros_full_version(milestone, short_version):
63 """Makes full_version from milestone and short_version"""
64 return 'R%s-%s' % (milestone, short_version)
65
66
67def version_split(full_version):
68 """Splits full_version into milestone and short_version"""
69 assert is_cros_full_version(full_version)
70 milestone, short_version = full_version.split('-')
71 return milestone[1:], short_version
72
73
74def argtype_cros_version(s):
75 if not is_cros_version(s):
76 msg = 'invalid cros version'
77 raise cli.ArgTypeError(msg, '9876.0.0 or R62-9876.0.0')
78 return s
79
80
81def query_dut_lsb_release(host):
82 """Query /etc/lsb-release of given DUT
83
84 Args:
85 host: the DUT address
86
87 Returns:
88 dict for keys and values of /etc/lsb-release. Return empty dict if failed.
89 """
90 try:
91 output = util.check_output('ssh', host, 'cat', '/etc/lsb-release')
92 except subprocess.CalledProcessError:
93 return {}
94 return dict(re.findall(r'^(\w+)=(.*)$', output, re.M))
95
96
97def is_dut(host):
Kuang-che Wubfc4a642018-04-19 11:54:08 +080098 """Determines whether a host is a chromeos device.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080099
100 Args:
101 host: the DUT address
102
103 Returns:
104 True if the host is a chromeos device.
105 """
106 return query_dut_lsb_release(host).get('DEVICETYPE') in [
107 'CHROMEBASE',
108 'CHROMEBIT',
109 'CHROMEBOOK',
110 'CHROMEBOX',
111 'REFERENCE',
112 ]
113
114
115def query_dut_board(host):
116 """Query board name of a given DUT"""
117 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_BOARD')
118
119
120def query_dut_short_version(host):
121 """Query short version of a given DUT"""
122 return query_dut_lsb_release(host).get('CHROMEOS_RELEASE_VERSION')
123
124
125def query_dut_boot_id(host, connect_timeout=None):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800126 """Query boot id.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800127
128 Args:
129 host: DUT address
130 connect_timeout: connection timeout
131
132 Returns:
133 boot uuid
134 """
135 cmd = ['ssh']
136 if connect_timeout:
137 cmd += ['-oConnectTimeout=%d' % connect_timeout]
138 cmd += [host, 'cat', '/proc/sys/kernel/random/boot_id']
139 return util.check_output(*cmd).strip()
140
141
142def reboot(host):
143 """Reboot a DUT and verify"""
144 logger.debug('reboot %s', host)
145 boot_id = query_dut_boot_id(host)
146
147 # Depends on timing, ssh may return failure due to broken pipe,
148 # so don't check ssh return code.
149 util.call('ssh', host, 'reboot')
Kuang-che Wu708310b2018-03-28 17:24:34 +0800150 wait_reboot_done(host, boot_id)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800151
Kuang-che Wu708310b2018-03-28 17:24:34 +0800152
153def wait_reboot_done(host, boot_id):
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800154 # For dev-mode test image, the reboot time is roughly at least 16 seconds
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800155 # (dev screen short delay) or more (long delay).
156 time.sleep(15)
157 for _ in range(100):
158 try:
159 # During boot, DUT does not response and thus ssh may hang a while. So
160 # set a connect timeout. 3 seconds are enough and 2 are not. It's okay to
161 # set tight limit because it's inside retry loop.
162 assert boot_id != query_dut_boot_id(host, connect_timeout=3)
163 return
164 except subprocess.CalledProcessError:
165 logger.debug('reboot not ready? sleep wait 1 sec')
166 time.sleep(1)
167
168 raise core.ExecutionFatalError('reboot failed?')
169
170
171def gsutil(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800172 """gsutil command line wrapper.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800173
174 Args:
175 args: command line arguments passed to gsutil
176 kwargs:
177 ignore_errors: if true, return '' for failures, for example 'gsutil ls'
178 but the path not found.
179
180 Returns:
181 stdout of gsutil
182
183 Raises:
184 core.ExecutionFatalError: gsutil failed to run
185 subprocess.CalledProcessError: command failed
186 """
187 stderr_lines = []
188 try:
189 return util.check_output(
190 gsutil_bin, *args, stderr_callback=stderr_lines.append)
191 except subprocess.CalledProcessError as e:
192 stderr = ''.join(stderr_lines)
193 if re.search(r'ServiceException:.* does not have .*access', stderr):
194 raise core.ExecutionFatalError(
195 'gsutil failed due to permission. ' +
196 'Run "%s config" and follow its instruction. ' % gsutil_bin +
197 'Fill any string if it asks for project-id')
198 if kwargs.get('ignore_errors'):
199 return ''
200 raise
201 except OSError as e:
202 if e.errno == errno.ENOENT:
203 raise core.ExecutionFatalError(
204 'Unable to run %s. gsutil is not installed or not in PATH?' %
205 gsutil_bin)
206 raise
207
208
209def gsutil_ls(*args, **kwargs):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800210 """gsutil ls.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800211
212 Args:
213 args: arguments passed to 'gsutil ls'
214 kwargs: extra parameters, where
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800215 ignore_errors: if true, return empty list instead of raising
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800216 exception, ex. path not found.
217
218 Returns:
219 list of 'gsutil ls' result. One element for one line of gsutil output.
220
221 Raises:
222 subprocess.CalledProcessError: gsutil failed, usually means path not found
223 """
224 return gsutil('ls', *args, **kwargs).splitlines()
225
226
227def query_milestone_by_version(board, short_version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800228 """Query milestone by ChromeOS version number.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800229
230 Args:
231 board: ChromeOS board name
232 short_version: ChromeOS version number in short format, ex. 9300.0.0
233
234 Returns:
235 ChromeOS milestone number (string). For example, '58' for '9300.0.0'.
236 None if failed.
237 """
238 path = gs_archive_path.format(board=board) + '/R*-' + short_version
239 for line in gsutil_ls('-d', path, ignore_errors=True):
240 m = re.search(r'/R(\d+)-', line)
241 if not m:
242 continue
243 return m.group(1)
244
245 for channel in ['canary', 'dev', 'beta', 'stable']:
246 path = gs_release_path.format(
247 channel=channel, board=board, short_version=short_version)
248 for line in gsutil_ls(path, ignore_errors=True):
249 m = re.search(r'\bR(\d+)-' + short_version, line)
250 if not m:
251 continue
252 return m.group(1)
253
254 logger.error('unable to query milestone of %s for %s', short_version, board)
255 return None
256
257
258def recognize_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800259 """Recognize ChromeOS version.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800260
261 Args:
262 board: ChromeOS board name
263 version: ChromeOS version number in short or full format
264
265 Returns:
266 (milestone, version in short format)
267 """
268 if is_cros_short_version(version):
269 milestone = query_milestone_by_version(board, version)
270 short_version = version
271 else:
272 milestone, short_version = version_split(version)
273 return milestone, short_version
274
275
276def version_to_short(version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800277 """Convert ChromeOS version number to short format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800278
279 Args:
280 version: ChromeOS version number in short or full format
281
282 Returns:
283 version number in short format
284 """
285 if is_cros_short_version(version):
286 return version
287 _, short_version = version_split(version)
288 return short_version
289
290
291def version_to_full(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800292 """Convert ChromeOS version number to full format.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800293
294 Args:
295 board: ChromeOS board name
296 version: ChromeOS version number in short or full format
297
298 Returns:
299 version number in full format
300 """
301 if is_cros_full_version(version):
302 return version
303 milestone = query_milestone_by_version(board, version)
304 return make_cros_full_version(milestone, version)
305
306
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800307def prepare_prebuilt_image(board, version):
308 """Prepare chromeos prebuilt image.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800309
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800310 It searches for xbuddy image which "cros flash" can use, or fetch image to
311 local disk.
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800312
313 Args:
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800314 board: ChromeOS board name
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800315 version: ChromeOS version number in short or full format
316
317 Returns:
318 xbuddy path or file path (outside chroot)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800319 """
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800320 assert is_cros_version(version)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800321 full_version = version_to_full(board, version)
322 short_version = version_to_short(full_version)
323
324 image_path = None
325 gs_path = gs_archive_path.format(board=board) + '/' + full_version
326 if gsutil_ls('-d', gs_path, ignore_errors=True):
327 image_path = 'xbuddy://remote/{board}/{full_version}/test'.format(
328 board=board, full_version=full_version)
329 else:
330 tmp_dir = 'tmp/ChromeOS-test-%s-%s' % (full_version, board)
331 if not os.path.exists(tmp_dir):
332 os.makedirs(tmp_dir)
333 # gs://chromeos-releases may have more old images than
Kuang-che Wu4fe945b2018-03-31 16:46:38 +0800334 # gs://chromeos-image-archive, but 'cros flash' doesn't support it. We have
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800335 # to fetch the image by ourselves
336 for channel in ['canary', 'dev', 'beta', 'stable']:
337 fn = 'ChromeOS-test-{full_version}-{board}.tar.xz'.format(
338 full_version=full_version, board=board)
339 gs_path = gs_release_path.format(
340 channel=channel, board=board, short_version=short_version)
341 gs_path += '/' + fn
342 if gsutil_ls(gs_path, ignore_errors=True):
343 # TODO(kcwu): delete tmp
344 gsutil('cp', gs_path, tmp_dir)
345 util.check_call('tar', 'Jxvf', fn, cwd=tmp_dir)
346 image_path = os.path.abspath(
347 os.path.join(tmp_dir, 'chromiumos_test_image.bin'))
348 break
349
350 assert image_path
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800351 return image_path
352
353
354def cros_flash(chromeos_root,
355 host,
356 board,
357 image_path,
358 version=None,
359 clobber_stateful=False,
360 disable_rootfs_verification=True,
361 run_inside_chroot=False):
362 """Flash a DUT with given ChromeOS image.
363
364 This is implemented by 'cros flash' command line.
365
366 Args:
367 chromeos_root: use 'cros flash' of which chromeos tree
368 host: DUT address
369 board: ChromeOS board name
370 image_path: chromeos image xbuddy path or file path. If
371 run_inside_chroot is True, the file path is relative to src/scrips.
372 Otherwise, the file path is relative to chromeos_root.
373 version: ChromeOS version in short or full format
374 clobber_stateful: Clobber stateful partition when performing update
375 disable_rootfs_verification: Disable rootfs verification after update
376 is completed
377 run_inside_chroot: if True, run 'cros flash' command inside the chroot
378 """
379 logger.info('cros_flash %s %s %s %s', host, board, version, image_path)
380
381 # Reboot is necessary because sometimes previous 'cros flash' failed and
382 # entered a bad state.
383 reboot(host)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800384
385 args = ['--no-ping', host, image_path]
386 if clobber_stateful:
387 args.append('--clobber-stateful')
388 if disable_rootfs_verification:
389 args.append('--disable-rootfs-verification')
390
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800391 if run_inside_chroot:
392 cros_sdk(chromeos_root, 'cros', 'flash', *args)
393 else:
394 util.check_call('chromite/bin/cros', 'flash', *args, cwd=chromeos_root)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800395
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800396 if version:
397 # In the past, cros flash may fail with returncode=0
398 # So let's have an extra check.
399 short_version = version_to_short(version)
400 dut_version = query_dut_short_version(host)
401 assert dut_version == short_version
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800402
403
404def version_info(board, version):
405 """Query subcomponents version info of given version of ChromeOS
406
407 Args:
408 board: ChromeOS board name
409 version: ChromeOS version number in short or full format
410
411 Returns:
412 dict of component and version info, including (if available):
413 cros_short_version: ChromeOS version
414 cros_full_version: ChromeOS version
415 milestone: milestone of ChromeOS
416 cr_version: Chrome version
Kuang-che Wu708310b2018-03-28 17:24:34 +0800417 android_build_id: Android build id
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800418 android_branch: Android branch, in format like 'git_nyc-mr1-arc'
419 """
420 info = {}
421 full_version = version_to_full(board, version)
422
423 # Some boards may have only partial-metadata.json but no metadata.json.
424 # e.g. caroline R60-9462.0.0
425 # Let's try both.
426 metadata = None
427 for metadata_filename in ['metadata.json', 'partial-metadata.json']:
428 path = gs_archive_path.format(board=board) + '/%s/%s' % (full_version,
429 metadata_filename)
430 metadata = gsutil('cat', path, ignore_errors=True)
431 if metadata:
432 o = json.loads(metadata)
433 v = o['version']
434 board_metadata = o['board-metadata'][board]
435 info.update({
436 VERSION_KEY_CROS_SHORT_VERSION: v['platform'],
437 VERSION_KEY_CROS_FULL_VERSION: v['full'],
438 VERSION_KEY_MILESTONE: v['milestone'],
439 VERSION_KEY_CR_VERSION: v['chrome'],
440 })
441
442 if 'android' in v:
Kuang-che Wu708310b2018-03-28 17:24:34 +0800443 info[VERSION_KEY_ANDROID_BUILD_ID] = v['android']
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800444 if 'android-branch' in v: # this appears since R58-9317.0.0
445 info[VERSION_KEY_ANDROID_BRANCH] = v['android-branch']
446 elif 'android-container-branch' in board_metadata:
447 info[VERSION_KEY_ANDROID_BRANCH] = v['android-container-branch']
448 break
449 else:
450 logger.error('Failed to read metadata from gs://chromeos-image-archive')
451 logger.error(
452 'Note, so far no quick way to look up version info for too old builds')
453
454 return info
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800455
456
457def query_chrome_version(board, version):
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800458 """Queries chrome version of chromeos build.
Kuang-che Wu848b1af2018-02-01 20:59:36 +0800459
460 Args:
461 board: ChromeOS board name
462 version: ChromeOS version number in short or full format
463
464 Returns:
465 Chrome version number
466 """
467 info = version_info(board, version)
468 return info['cr_version']
Kuang-che Wu708310b2018-03-28 17:24:34 +0800469
470
471def query_android_build_id(board, rev):
472 info = version_info(board, rev)
473 rev = info['android_build_id']
474 return rev
475
476
477def query_android_branch(board, rev):
478 info = version_info(board, rev)
479 rev = info['android_branch']
480 return rev
Kuang-che Wubfc4a642018-04-19 11:54:08 +0800481
482
483def is_inside_chroot():
484 """Returns True if we are inside chroot."""
485 return os.path.exists('/etc/cros_chroot_version')
486
487
488def cros_sdk(chromeos_root, *args, **kwargs):
489 """Run commands inside chromeos chroot.
490
491 Args:
492 chromeos_root: chromeos tree root
493 *args: command to run
494 **kwargs:
495 env: (dict) environment variables for the command
496 stdin: standard input file handle for the command
497 """
498 envs = []
499 for k, v in kwargs.get('env', {}).items():
500 assert re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', k)
501 envs.append('%s=%s' % (k, v))
502
503 # Use --no-ns-pid to prevent cros_sdk change our pgid, otherwise subsequent
504 # commands would be considered as background process.
505 cmd = ['chromite/bin/cros_sdk', '--no-ns-pid'] + envs + ['--'] + list(args)
506 return util.check_output(*cmd, cwd=chromeos_root, stdin=kwargs.get('stdin'))
507
508
509def copy_into_chroot(chromeos_root, src, dst):
510 """Copies file into chromeos chroot.
511
512 Args:
513 chromeos_root: chromeos tree root
514 src: path outside chroot
515 dst: path inside chroot
516 """
517 # chroot may be an image, so we cannot copy to corresponding path
518 # directly.
519 cros_sdk(chromeos_root, 'sh', '-c', 'cat > %s' % dst, stdin=open(src))
520
521
522def exists_in_chroot(chromeos_root, path):
523 """Determine whether a path exists in the chroot.
524
525 Args:
526 chromeos_root: chromeos tree root
527 path: path inside chroot, relative to src/scripts
528
529 Returns:
530 True if a path exists
531 """
532 try:
533 cros_sdk(chromeos_root, '[', '-e', path, ']')
534 except subprocess.CalledProcessError:
535 return False
536 return True
537
538
539def build_image(chromeos_root, board, rev):
540 """Build ChromeOS image.
541
542 Args:
543 chromeos_root: chromeos tree root
544 board: ChromeOS board name
545 rev: the version name to build
546
547 Returns:
548 Image path
549 """
550
551 # If the given version is already built, reuse it.
552 image_name = 'bisect-%s' % rev
553 image_path = os.path.join('../build/images', board, image_name,
554 'chromiumos_test_image.bin')
555 if exists_in_chroot(chromeos_root, image_path):
556 logger.info('"%s" already exists, skip build step', image_path)
557 return image_path
558
559 dirname = os.path.dirname(os.path.abspath(__file__))
560 script_name = 'build_cros_helper.sh'
561 copy_into_chroot(chromeos_root, os.path.join(dirname, '..', script_name),
562 script_name)
563 cros_sdk(chromeos_root, 'chmod', '+x', script_name)
564 cros_sdk(chromeos_root, './%s' % script_name, board, image_name)
565 return image_path
566
567
568class ChromeOSManifestManager(repo_util.ManifestManager):
569 """Manifest operations for chromeos repo"""
570
571 def __init__(self, config):
572 self.config = config
573
574 def _query_manifest_name(self, rev):
575 assert is_cros_full_version(rev)
576 milestone, short_version = version_split(rev)
577 manifest_name = os.path.join('buildspecs', milestone,
578 '%s.xml' % short_version)
579 return manifest_name
580
581 def sync_disk_state(self, rev):
582 manifest_name = self._query_manifest_name(rev)
583
584 # For ChromeOS, mark_as_stable step requires 'repo init -m', which sticks
585 # manifest. 'repo sync -m' is not enough
586 repo_util.init(
587 self.config['repo_dir'],
588 'https://chrome-internal.googlesource.com/chromeos/manifest-versions/',
589 manifest_name=manifest_name,
590 repo_url='https://chromium.googlesource.com/external/repo.git')
591
592 # Note, don't sync with current_branch=True for chromeos. One of its
593 # build steps (inside mark_as_stable) executes "git describe" which
594 # needs git tag information.
595 repo_util.sync(self.config['repo_dir'])
596
597 def fetch_git_repos(self, rev):
598 # Early return if necessary git history are already fetched.
599 mf = self.fetch_manifest(rev)
600 repo_set = repo_util.parse_repo_set(self.config['repo_dir'], mf)
601 all_have = True
602 for path, git_rev in repo_set.items():
603 git_root = os.path.join(self.config['repo_dir'], path)
604 if not os.path.exists(git_root):
605 all_have = False
606 break
607 if not git_util.is_containing_commit(git_root, git_rev):
608 all_have = False
609 break
610 if all_have:
611 return
612
613 # TODO(kcwu): fetch git history but don't switch current disk state.
614 self.sync_disk_state(rev)
615
616 def enumerate_manifest(self, old, new):
617 assert is_cros_full_version(old)
618 assert is_cros_full_version(new)
619 old_milestone, old_short_version = version_split(old)
620 new_milestone, new_short_version = version_split(new)
621
622 # TODO(kcwu): fetch manifests but don't switch current disk state.
623 self.sync_disk_state(new)
624
625 spec_dir = os.path.join(self.config['repo_dir'], '.repo', 'manifests',
626 'buildspecs')
627 result = []
628 for root, dirs, files in os.walk(spec_dir):
629 dirs[:] = [
630 dn for dn in dirs if dn.isdigit() and
631 int(old_milestone) <= int(dn) <= int(new_milestone)
632 ]
633
634 for fn in files:
635 short_version, ext = os.path.splitext(fn)
636 if ext != '.xml':
637 continue
638 milestone = os.path.basename(root)
639 if (util.is_version_lesseq(old_short_version, short_version) and
640 util.is_version_lesseq(short_version, new_short_version) and
641 util.is_direct_relative_version(short_version, new_short_version)):
642 result.append(make_cros_full_version(milestone, short_version))
643
644 def version_key_func(full_version):
645 _milestone, short_version = version_split(full_version)
646 return util.version_key_func(short_version)
647
648 result.sort(key=version_key_func)
649 assert result[0] == old
650 assert result[-1] == new
651 return result
652
653 def fetch_manifest(self, rev):
654 # The manifest is already synced, no need to fetch.
655 return self._query_manifest_name(rev)