autoupdater.py revision c2a15ebfadd2a3cf4ab369b3d0c4a868ce99505c
1# Copyright (c) 2012 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 5import glob 6import httplib 7import logging 8import multiprocessing 9import os 10import re 11import urlparse 12import urllib2 13 14from autotest_lib.client.bin import utils 15from autotest_lib.client.common_lib import error, global_config 16from autotest_lib.client.common_lib.cros import dev_server 17from autotest_lib.client.common_lib.cros.graphite import autotest_stats 18 19 20# Local stateful update path is relative to the CrOS source directory. 21LOCAL_STATEFUL_UPDATE_PATH = 'src/platform/dev/stateful_update' 22LOCAL_CHROOT_STATEFUL_UPDATE_PATH = '/usr/bin/stateful_update' 23UPDATER_IDLE = 'UPDATE_STATUS_IDLE' 24UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' 25# A list of update engine client states that occur after an update is triggered. 26UPDATER_PROCESSING_UPDATE = ['UPDATE_STATUS_CHECKING_FORUPDATE', 27 'UPDATE_STATUS_UPDATE_AVAILABLE', 28 'UPDATE_STATUS_DOWNLOADING', 29 'UPDATE_STATUS_FINALIZING'] 30 31class ChromiumOSError(error.InstallError): 32 """Generic error for ChromiumOS-specific exceptions.""" 33 34 35class BrilloError(error.InstallError): 36 """Generic error for Brillo-specific exceptions.""" 37 38 39class RootFSUpdateError(ChromiumOSError): 40 """Raised when the RootFS fails to update.""" 41 42 43class StatefulUpdateError(ChromiumOSError): 44 """Raised when the stateful partition fails to update.""" 45 46 47def url_to_version(update_url): 48 """Return the version based on update_url. 49 50 @param update_url: url to the image to update to. 51 52 """ 53 # The Chrome OS version is generally the last element in the URL. The only 54 # exception is delta update URLs, which are rooted under the version; e.g., 55 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to 56 # strip off the au section of the path before reading the version. 57 return re.sub('/au/.*', '', 58 urlparse.urlparse(update_url).path).split('/')[-1].strip() 59 60 61def url_to_image_name(update_url): 62 """Return the image name based on update_url. 63 64 From a URL like: 65 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0 66 return lumpy-release/R27-3837.0.0 67 68 @param update_url: url to the image to update to. 69 @returns a string representing the image name in the update_url. 70 71 """ 72 return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:]) 73 74 75def _get_devserver_build_from_update_url(update_url): 76 """Get the devserver and build from the update url. 77 78 @param update_url: The url for update. 79 Eg: http://devserver:port/update/build. 80 81 @return: A tuple of (devserver url, build) or None if the update_url 82 doesn't match the expected pattern. 83 84 @raises ValueError: If the update_url doesn't match the expected pattern. 85 @raises ValueError: If no global_config was found, or it doesn't contain an 86 image_url_pattern. 87 """ 88 pattern = global_config.global_config.get_config_value( 89 'CROS', 'image_url_pattern', type=str, default='') 90 if not pattern: 91 raise ValueError('Cannot parse update_url, the global config needs ' 92 'an image_url_pattern.') 93 re_pattern = pattern.replace('%s', '(\S+)') 94 parts = re.search(re_pattern, update_url) 95 if not parts or len(parts.groups()) < 2: 96 raise ValueError('%s is not an update url' % update_url) 97 return parts.groups() 98 99 100def list_image_dir_contents(update_url): 101 """Lists the contents of the devserver for a given build/update_url. 102 103 @param update_url: An update url. Eg: http://devserver:port/update/build. 104 """ 105 if not update_url: 106 logging.warning('Need update_url to list contents of the devserver.') 107 return 108 error_msg = 'Cannot check contents of devserver, update url %s' % update_url 109 try: 110 devserver_url, build = _get_devserver_build_from_update_url(update_url) 111 except ValueError as e: 112 logging.warning('%s: %s', error_msg, e) 113 return 114 devserver = dev_server.ImageServer(devserver_url) 115 try: 116 devserver.list_image_dir(build) 117 # The devserver will retry on URLError to avoid flaky connections, but will 118 # eventually raise the URLError if it persists. All HTTPErrors get 119 # converted to DevServerExceptions. 120 except (dev_server.DevServerException, urllib2.URLError) as e: 121 logging.warning('%s: %s', error_msg, e) 122 123 124# TODO(garnold) This implements shared updater functionality needed for 125# supporting the autoupdate_EndToEnd server-side test. We should probably 126# migrate more of the existing ChromiumOSUpdater functionality to it as we 127# expand non-CrOS support in other tests. 128class BaseUpdater(object): 129 """Platform-agnostic DUT update functionality.""" 130 131 def __init__(self, updater_ctrl_bin, update_url, host): 132 """Initializes the object. 133 134 @param updater_ctrl_bin: Path to update_engine_client. 135 @param update_url: The URL we want the update to use. 136 @param host: A client.common_lib.hosts.Host implementation. 137 """ 138 self.updater_ctrl_bin = updater_ctrl_bin 139 self.update_url = update_url 140 self.host = host 141 self._update_error_queue = multiprocessing.Queue(2) 142 143 144 def check_update_status(self): 145 """Returns the current update engine state. 146 147 We use the `update_engine_client -status' command and parse the line 148 indicating the update state, e.g. "CURRENT_OP=UPDATE_STATUS_IDLE". 149 """ 150 update_status = self.host.run( 151 '%s -status 2>&1 | grep CURRENT_OP' % self.updater_ctrl_bin) 152 return update_status.stdout.strip().split('=')[-1] 153 154 155 def get_last_update_error(self): 156 """Get the last autoupdate error code.""" 157 error_msg = self.host.run( 158 '%s --last_attempt_error' % self.updater_ctrl_bin) 159 error_msg = (error_msg.stdout.strip()).replace('\n', ', ') 160 return error_msg 161 162 163 def _base_update_handler(self, run_args, err_msg_prefix=None): 164 """Base function to handle a remote update ssh call. 165 166 @param run_args: Dictionary of args passed to ssh_host.run function. 167 @param err_msg_prefix: Prefix of the exception error message. 168 169 @returns: The exception thrown, None if no exception. 170 """ 171 to_raise = None 172 err_msg = err_msg_prefix 173 try: 174 self.host.run(**run_args) 175 except (error.AutoservSshPermissionDeniedError, 176 error.AutoservSSHTimeout) as e: 177 logging.exception(e) 178 err_msg += 'SSH reports an error: %s' % type(e).__name__ 179 to_raise = RootFSUpdateError(err_msg) 180 except error.AutoservRunError as e: 181 logging.exception(e) 182 # Check if exit code is 255, if so it's probably a generic SSH error 183 result = e.args[1] 184 if result.exit_status == 255: 185 err_msg += ('SSH reports a generic error (255) which is ' 186 'probably a lab network failure') 187 to_raise = RootFSUpdateError(err_msg) 188 189 # We have ruled out all SSH cases, the error code is from 190 # update_engine_client. 191 else: 192 list_image_dir_contents(self.update_url) 193 err_msg += ('Update failed. Returned update_engine error code: ' 194 '%s. Reported error: %s' % 195 (self.get_last_update_error(), type(e).__name__)) 196 to_raise = RootFSUpdateError(err_msg) 197 except Exception as e: 198 to_raise = e 199 200 return to_raise 201 202 203 def trigger_update(self): 204 """Triggers a background update. 205 206 @raise RootFSUpdateError or unknown Exception if anything went wrong. 207 """ 208 autoupdate_cmd = ('%s --check_for_update --omaha_url=%s' % 209 (self.updater_ctrl_bin, self.update_url)) 210 run_args = {'command': autoupdate_cmd} 211 err_prefix = 'Failed to trigger an update on %s. ' % self.host.hostname 212 logging.info('Triggering update via: %s', autoupdate_cmd) 213 to_raise = self._base_update_handler(run_args, err_prefix) 214 if to_raise: 215 raise to_raise 216 217 218 def _verify_update_completed(self): 219 """Verifies that an update has completed. 220 221 @raise RootFSUpdateError: if verification fails. 222 """ 223 status = self.check_update_status() 224 if status != UPDATER_NEED_REBOOT: 225 error_msg = '' 226 if status == UPDATER_IDLE: 227 error_msg = 'Update error: %s' % self.get_last_update_error() 228 raise RootFSUpdateError('Update did not complete with correct ' 229 'status. Expecting %s, actual %s. %s' % 230 (UPDATER_NEED_REBOOT, status, error_msg)) 231 232 233 def update_image(self): 234 """Updates the device image and verifies success.""" 235 autoupdate_cmd = ('%s --update --omaha_url=%s 2>&1' % 236 (self.updater_ctrl_bin, self.update_url)) 237 run_args = {'command': autoupdate_cmd, 'timeout': 3600} 238 err_prefix = ('Failed to install device image using payload at %s ' 239 'on %s. ' % (self.update_url, self.host.hostname)) 240 logging.info('Updating image via: %s', autoupdate_cmd) 241 to_raise = self._base_update_handler(run_args, err_prefix) 242 if to_raise: 243 self._update_error_queue.put(to_raise) 244 raise to_raise 245 246 try: 247 self._verify_update_completed() 248 except RootFSUpdateError as e: 249 self._update_error_queue.put(e) 250 raise 251 252 253class ChromiumOSUpdater(BaseUpdater): 254 """Helper class used to update DUT with image of desired version.""" 255 REMOTE_STATEUL_UPDATE_PATH = '/usr/local/bin/stateful_update' 256 UPDATER_BIN = '/usr/bin/update_engine_client' 257 STATEFUL_UPDATE = '/tmp/stateful_update' 258 UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed' 259 UPDATER_LOGS = ['/var/log/messages', '/var/log/update_engine'] 260 261 KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3} 262 KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5} 263 # Time to wait for new kernel to be marked successful after 264 # auto update. 265 KERNEL_UPDATE_TIMEOUT = 120 266 267 _timer = autotest_stats.Timer('cros_autoupdater') 268 269 def __init__(self, update_url, host=None, local_devserver=False): 270 super(ChromiumOSUpdater, self).__init__(self.UPDATER_BIN, update_url, 271 host) 272 self.local_devserver = local_devserver 273 if not local_devserver: 274 self.update_version = url_to_version(update_url) 275 else: 276 self.update_version = None 277 278 279 def reset_update_engine(self): 280 """Resets the host to prepare for a clean update regardless of state.""" 281 self._run('rm -f %s' % self.UPDATED_MARKER) 282 self._run('stop ui || true') 283 self._run('stop update-engine || true') 284 self._run('start update-engine') 285 286 if self.check_update_status() != UPDATER_IDLE: 287 raise ChromiumOSError('%s is not in an installable state' % 288 self.host.hostname) 289 290 291 def _run(self, cmd, *args, **kwargs): 292 """Abbreviated form of self.host.run(...)""" 293 return self.host.run(cmd, *args, **kwargs) 294 295 296 def rootdev(self, options=''): 297 """Returns the stripped output of rootdev <options>. 298 299 @param options: options to run rootdev. 300 301 """ 302 return self._run('rootdev %s' % options).stdout.strip() 303 304 305 def get_kernel_state(self): 306 """Returns the (<active>, <inactive>) kernel state as a pair.""" 307 active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0]) 308 if active_root == self.KERNEL_A['root']: 309 return self.KERNEL_A, self.KERNEL_B 310 elif active_root == self.KERNEL_B['root']: 311 return self.KERNEL_B, self.KERNEL_A 312 else: 313 raise ChromiumOSError('Encountered unknown root partition: %s' % 314 active_root) 315 316 317 def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'): 318 """Return numeric cgpt value for the specified flag, kernel, device. """ 319 return int(self._run('cgpt show -n -i %d %s %s' % ( 320 kernel['kernel'], flag, dev)).stdout.strip()) 321 322 323 def get_kernel_priority(self, kernel): 324 """Return numeric priority for the specified kernel. 325 326 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. 327 328 """ 329 return self._cgpt('-P', kernel) 330 331 332 def get_kernel_success(self, kernel): 333 """Return boolean success flag for the specified kernel. 334 335 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. 336 337 """ 338 return self._cgpt('-S', kernel) != 0 339 340 341 def get_kernel_tries(self, kernel): 342 """Return tries count for the specified kernel. 343 344 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B. 345 346 """ 347 return self._cgpt('-T', kernel) 348 349 350 def get_stateful_update_script(self): 351 """Returns the path to the stateful update script on the target.""" 352 # We attempt to load the local stateful update path in 3 different 353 # ways. First we use the location specified in the autotest global 354 # config. If this doesn't exist, we attempt to use the Chromium OS 355 # Chroot path to the installed script. If all else fails, we use the 356 # stateful update script on the host. 357 stateful_update_path = os.path.join( 358 global_config.global_config.get_config_value( 359 'CROS', 'source_tree', default=''), 360 LOCAL_STATEFUL_UPDATE_PATH) 361 362 if not os.path.exists(stateful_update_path): 363 logging.warning('Could not find Chrome OS source location for ' 364 'stateful_update script at %s, falling back to ' 365 'chroot copy.', stateful_update_path) 366 stateful_update_path = LOCAL_CHROOT_STATEFUL_UPDATE_PATH 367 368 if not os.path.exists(stateful_update_path): 369 logging.warning('Could not chroot stateful_update script, falling ' 370 'back on client copy.') 371 statefuldev_script = self.REMOTE_STATEUL_UPDATE_PATH 372 else: 373 self.host.send_file( 374 stateful_update_path, self.STATEFUL_UPDATE, 375 delete_dest=True) 376 statefuldev_script = self.STATEFUL_UPDATE 377 378 return statefuldev_script 379 380 381 def reset_stateful_partition(self): 382 """Clear any pending stateful update request.""" 383 statefuldev_cmd = [self.get_stateful_update_script()] 384 statefuldev_cmd += ['--stateful_change=reset', '2>&1'] 385 self._run(' '.join(statefuldev_cmd)) 386 387 388 def revert_boot_partition(self): 389 """Revert the boot partition.""" 390 part = self.rootdev('-s') 391 logging.warning('Reverting update; Boot partition will be %s', part) 392 return self._run('/postinst %s 2>&1' % part) 393 394 395 def rollback_rootfs(self, powerwash): 396 """Triggers rollback and waits for it to complete. 397 398 @param powerwash: If true, powerwash as part of rollback. 399 400 @raise RootFSUpdateError if anything went wrong. 401 402 """ 403 version = self.host.get_release_version() 404 # Introduced can_rollback in M36 (build 5772). # etc/lsb-release matches 405 # X.Y.Z. This version split just pulls the first part out. 406 try: 407 build_number = int(version.split('.')[0]) 408 except ValueError: 409 logging.error('Could not parse build number.') 410 build_number = 0 411 412 if build_number >= 5772: 413 can_rollback_cmd = '%s --can_rollback' % self.UPDATER_BIN 414 logging.info('Checking for rollback.') 415 try: 416 self._run(can_rollback_cmd) 417 except error.AutoservRunError as e: 418 raise RootFSUpdateError("Rollback isn't possible on %s: %s" % 419 (self.host.hostname, str(e))) 420 421 rollback_cmd = '%s --rollback --follow' % self.UPDATER_BIN 422 if not powerwash: 423 rollback_cmd += ' --nopowerwash' 424 425 logging.info('Performing rollback.') 426 try: 427 self._run(rollback_cmd) 428 except error.AutoservRunError as e: 429 raise RootFSUpdateError('Rollback failed on %s: %s' % 430 (self.host.hostname, str(e))) 431 432 self._verify_update_completed() 433 434 435 # TODO(garnold) This is here for backward compatibility and should be 436 # deprecated once we shift to using update_image() everywhere. 437 @_timer.decorate 438 def update_rootfs(self): 439 """Run the standard command to force an update.""" 440 return self.update_image() 441 442 443 @_timer.decorate 444 def update_stateful(self, clobber=True): 445 """Updates the stateful partition. 446 447 @param clobber: If True, a clean stateful installation. 448 """ 449 logging.info('Updating stateful partition...') 450 statefuldev_url = self.update_url.replace('update', 451 'static') 452 453 # Attempt stateful partition update; this must succeed so that the newly 454 # installed host is testable after update. 455 statefuldev_cmd = [self.get_stateful_update_script(), statefuldev_url] 456 if clobber: 457 statefuldev_cmd.append('--stateful_change=clean') 458 459 statefuldev_cmd.append('2>&1') 460 try: 461 self._run(' '.join(statefuldev_cmd), timeout=1200) 462 except error.AutoservRunError: 463 update_error = StatefulUpdateError( 464 'Failed to perform stateful update on %s' % 465 self.host.hostname) 466 self._update_error_queue.put(update_error) 467 raise update_error 468 except Exception as e: 469 # Don't allow other exceptions to not be caught. 470 self._update_error_queue.put(e) 471 raise e 472 473 474 @_timer.decorate 475 def run_update(self, update_root=True): 476 """Update the DUT with image of specific version. 477 478 @param update_root: True to force a rootfs update. 479 """ 480 booted_version = self.host.get_release_version() 481 if self.update_version: 482 logging.info('Updating from version %s to %s.', 483 booted_version, self.update_version) 484 485 # Check that Dev Server is accepting connections (from autoserv's host). 486 # If we can't talk to it, the machine host probably can't either. 487 auserver_host = urlparse.urlparse(self.update_url)[1] 488 try: 489 httplib.HTTPConnection(auserver_host).connect() 490 except IOError: 491 raise ChromiumOSError( 492 'Update server at %s not available' % auserver_host) 493 494 logging.info('Installing from %s to %s', self.update_url, 495 self.host.hostname) 496 497 # Reset update state. 498 self.reset_update_engine() 499 self.reset_stateful_partition() 500 501 try: 502 updaters = [ 503 multiprocessing.process.Process(target=self.update_rootfs), 504 multiprocessing.process.Process(target=self.update_stateful) 505 ] 506 if not update_root: 507 logging.info('Root update is skipped.') 508 updaters = updaters[1:] 509 510 # Run the updaters in parallel. 511 for updater in updaters: updater.start() 512 for updater in updaters: updater.join() 513 514 # Re-raise the first error that occurred. 515 if not self._update_error_queue.empty(): 516 update_error = self._update_error_queue.get() 517 self.revert_boot_partition() 518 self.reset_stateful_partition() 519 raise update_error 520 521 logging.info('Update complete.') 522 except: 523 # Collect update engine logs in the event of failure. 524 if self.host.job: 525 logging.info('Collecting update engine logs...') 526 self.host.get_file( 527 self.UPDATER_LOGS, self.host.job.sysinfo.sysinfodir, 528 preserve_perm=False) 529 list_image_dir_contents(self.update_url) 530 raise 531 finally: 532 logging.info('Update engine log has downloaded in ' 533 'sysinfo/update_engine dir. Check the lastest.') 534 535 536 def check_version(self): 537 """Check the image running in DUT has the desired version. 538 539 @returns: True if the DUT's image version matches the version that 540 the autoupdater tries to update to. 541 542 """ 543 booted_version = self.host.get_release_version() 544 return (self.update_version and 545 self.update_version.endswith(booted_version)) 546 547 548 def check_version_to_confirm_install(self): 549 """Check image running in DUT has the desired version to be installed. 550 551 The method should not be used to check if DUT needs to have a full 552 reimage. Only use it to confirm a image is installed. 553 554 The method is designed to verify version for following 6 scenarios with 555 samples of version to update to and expected booted version: 556 1. trybot paladin build. 557 update version: trybot-lumpy-paladin/R27-3837.0.0-b123 558 booted version: 3837.0.2013_03_21_1340 559 560 2. trybot release build. 561 update version: trybot-lumpy-release/R27-3837.0.0-b456 562 booted version: 3837.0.0 563 564 3. buildbot official release build. 565 update version: lumpy-release/R27-3837.0.0 566 booted version: 3837.0.0 567 568 4. non-official paladin rc build. 569 update version: lumpy-paladin/R27-3878.0.0-rc7 570 booted version: 3837.0.0-rc7 571 572 5. chrome-perf build. 573 update version: lumpy-chrome-perf/R28-3837.0.0-b2996 574 booted version: 3837.0.0 575 576 6. pgo-generate build. 577 update version: lumpy-release-pgo-generate/R28-3837.0.0-b2996 578 booted version: 3837.0.0-pgo-generate 579 580 When we are checking if a DUT needs to do a full install, we should NOT 581 use this method to check if the DUT is running the same version, since 582 it may return false positive for a DUT running trybot paladin build to 583 be updated to another trybot paladin build. 584 585 TODO: This logic has a bug if a trybot paladin build failed to be 586 installed in a DUT running an older trybot paladin build with same 587 platform number, but different build number (-b###). So to conclusively 588 determine if a tryjob paladin build is imaged successfully, we may need 589 to find out the date string from update url. 590 591 @returns: True if the DUT's image version (without the date string if 592 the image is a trybot build), matches the version that the 593 autoupdater is trying to update to. 594 595 """ 596 # In the local_devserver case, we can't know the expected 597 # build, so just pass. 598 if not self.update_version: 599 return True 600 601 # Always try the default check_version method first, this prevents 602 # any backward compatibility issue. 603 if self.check_version(): 604 return True 605 606 return utils.version_match(self.update_version, 607 self.host.get_release_version(), 608 self.update_url) 609 610 611 def verify_boot_expectations(self, expected_kernel_state, rollback_message): 612 """Verifies that we fully booted given expected kernel state. 613 614 This method both verifies that we booted using the correct kernel 615 state and that the OS has marked the kernel as good. 616 617 @param expected_kernel_state: kernel state that we are verifying with 618 i.e. I expect to be booted onto partition 4 etc. See output of 619 get_kernel_state. 620 @param rollback_message: string to raise as a ChromiumOSError 621 if we booted with the wrong partition. 622 623 @raises ChromiumOSError: If we didn't. 624 """ 625 # Figure out the newly active kernel. 626 active_kernel_state = self.get_kernel_state()[0] 627 628 # Check for rollback due to a bad build. 629 if (expected_kernel_state and 630 active_kernel_state != expected_kernel_state): 631 632 # Kernel crash reports should be wiped between test runs, but 633 # may persist from earlier parts of the test, or from problems 634 # with provisioning. 635 # 636 # Kernel crash reports will NOT be present if the crash happened 637 # before encrypted stateful is mounted. 638 # 639 # TODO(dgarrett): Integrate with server/crashcollect.py at some 640 # point. 641 kernel_crashes = glob.glob('/var/spool/crash/kernel.*.kcrash') 642 if kernel_crashes: 643 rollback_message += ': kernel_crash' 644 logging.debug('Found %d kernel crash reports:', 645 len(kernel_crashes)) 646 # The crash names contain timestamps that may be useful: 647 # kernel.20131207.005945.0.kcrash 648 for crash in kernel_crashes: 649 logging.debug(' %s', os.path.basename(crash)) 650 651 # Print out some information to make it easier to debug 652 # the rollback. 653 logging.debug('Dumping partition table.') 654 self._run('cgpt show $(rootdev -s -d)') 655 logging.debug('Dumping crossystem for firmware debugging.') 656 self._run('crossystem --all') 657 raise ChromiumOSError(rollback_message) 658 659 # Make sure chromeos-setgoodkernel runs. 660 try: 661 utils.poll_for_condition( 662 lambda: (self.get_kernel_tries(active_kernel_state) == 0 663 and self.get_kernel_success(active_kernel_state)), 664 exception=ChromiumOSError(), 665 timeout=self.KERNEL_UPDATE_TIMEOUT, sleep_interval=5) 666 except ChromiumOSError: 667 services_status = self._run('status system-services').stdout 668 if services_status != 'system-services start/running\n': 669 event = ('Chrome failed to reach login screen') 670 else: 671 event = ('update-engine failed to call ' 672 'chromeos-setgoodkernel') 673 raise ChromiumOSError( 674 'After update and reboot, %s ' 675 'within %d seconds' % (event, 676 self.KERNEL_UPDATE_TIMEOUT)) 677 678 679class BrilloUpdater(BaseUpdater): 680 """Helper class for updating a Brillo DUT.""" 681 682 def __init__(self, update_url, host=None): 683 """Initialize the object. 684 685 @param update_url: The URL we want the update to use. 686 @param host: A client.common_lib.hosts.Host implementation. 687 """ 688 super(BrilloUpdater, self).__init__( 689 '/system/bin/update_engine_client', update_url, host) 690