1# Copyright 2018 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 datetime 6import logging 7import os 8import re 9import shutil 10import time 11import urlparse 12 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.common_lib import utils 15from autotest_lib.client.common_lib.cros import kernel_utils 16from autotest_lib.client.cros.update_engine import update_engine_event 17 18_DEFAULT_RUN = utils.run 19_DEFAULT_COPY = shutil.copy 20 21class UpdateEngineUtil(object): 22 """ 23 Utility code shared between client and server update_engine autotests. 24 25 All update_engine autotests inherit from either the client or server 26 version of update_engine_test: 27 client/cros/update_engine/update_engine_test.py 28 server/cros/update_engine/update_engine_test.py 29 30 These update_engine_test classes inherit from test and update_engine_util. 31 For update_engine_util to work seamlessly, we need the client and server 32 update_engine_tests to define _run() and _get_file() functions: 33 server side: host.run and host.get_file 34 client side: utils.run and shutil.copy 35 36 """ 37 38 # Update engine status lines. 39 _PROGRESS = 'PROGRESS' 40 _CURRENT_OP = 'CURRENT_OP' 41 42 # Source version when we force an update. 43 _FORCED_UPDATE = 'ForcedUpdate' 44 45 # update_engine_client command 46 _UPDATE_ENGINE_CLIENT_CMD = 'update_engine_client' 47 48 # Update engine statuses. 49 _UPDATE_STATUS_IDLE = 'UPDATE_STATUS_IDLE' 50 _UPDATE_STATUS_CHECKING_FOR_UPDATE = 'UPDATE_STATUS_CHECKING_FOR_UPDATE' 51 _UPDATE_STATUS_UPDATE_AVAILABLE = 'UPDATE_STATUS_UPDATE_AVAILABLE' 52 _UPDATE_STATUS_DOWNLOADING = 'UPDATE_STATUS_DOWNLOADING' 53 _UPDATE_STATUS_FINALIZING = 'UPDATE_STATUS_FINALIZING' 54 _UPDATE_STATUS_UPDATED_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' 55 _UPDATE_STATUS_REPORTING_ERROR_EVENT = 'UPDATE_STATUS_REPORTING_ERROR_EVENT' 56 57 # Files used by the tests. 58 _UPDATE_ENGINE_LOG = '/var/log/update_engine.log' 59 _UPDATE_ENGINE_LOG_DIR = '/var/log/update_engine/' 60 _CUSTOM_LSB_RELEASE = '/mnt/stateful_partition/etc/lsb-release' 61 _UPDATE_ENGINE_PREFS_DIR = '/var/lib/update_engine/prefs/' 62 63 # Update engine prefs 64 _UPDATE_CHECK_RESPONSE_HASH = 'update-check-response-hash' 65 66 # Interrupt types supported in AU tests. 67 _REBOOT_INTERRUPT = 'reboot' 68 _SUSPEND_INTERRUPT = 'suspend' 69 _NETWORK_INTERRUPT = 'network' 70 _SUPPORTED_INTERRUPTS = [_REBOOT_INTERRUPT, _SUSPEND_INTERRUPT, 71 _NETWORK_INTERRUPT] 72 73 # Public key used to force update_engine to verify omaha response data on 74 # test images. 75 _IMAGE_PUBLIC_KEY = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZE03Z25kNDNjV2ZRenlydDE2UQpESEUrVDB5eGcxOE9aTys5c2M4aldwakMxekZ0b01Gb2tFU2l1OVRMVXArS1VDMjc0ZitEeElnQWZTQ082VTVECkpGUlBYVXp2ZTF2YVhZZnFsalVCeGMrSlljR2RkNlBDVWw0QXA5ZjAyRGhrckduZi9ya0hPQ0VoRk5wbTUzZG8Kdlo5QTZRNUtCZmNnMUhlUTA4OG9wVmNlUUd0VW1MK2JPTnE1dEx2TkZMVVUwUnUwQW00QURKOFhtdzRycHZxdgptWEphRm1WdWYvR3g3K1RPbmFKdlpUZU9POUFKSzZxNlY4RTcrWlppTUljNUY0RU9zNUFYL2xaZk5PM1JWZ0cyCk83RGh6emErbk96SjNaSkdLNVI0V3daZHVobjlRUllvZ1lQQjBjNjI4NzhxWHBmMkJuM05wVVBpOENmL1JMTU0KbVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==' 76 77 # Screenshot names used by interrupt tests 78 _BEFORE_INTERRUPT_FILENAME = 'before_interrupt.png' 79 _AFTER_INTERRUPT_FILENAME = 'after_interrupt.png' 80 81 82 def __init__(self, run_func=_DEFAULT_RUN, get_file=_DEFAULT_COPY): 83 """ 84 Initialize this class with _run() and _get_file() functions. 85 86 @param run_func: the function to use to run commands on the client. 87 Defaults for use by client tests, but can be 88 overwritten to run remotely from a server test. 89 @param get_file: the function to use when copying a file. Defaults 90 for use by client tests. Called with 91 (file, destination) syntax. 92 93 """ 94 self._set_util_functions(run_func, get_file) 95 96 97 def _set_util_functions(self, run_func=_DEFAULT_RUN, 98 get_file=_DEFAULT_COPY): 99 """See __init__().""" 100 self._run = run_func 101 self._get_file = get_file 102 103 104 def _wait_for_progress(self, progress): 105 """ 106 Waits until we reach the percentage passed as the input. 107 108 @param progress: The progress [0.0 .. 1.0] we want to wait for. 109 """ 110 while True: 111 completed = self._get_update_progress() 112 logging.debug('Checking if %s is greater than %s', completed, 113 progress) 114 if completed >= progress: 115 break 116 time.sleep(1) 117 118 119 def _is_update_started(self): 120 """Checks if the update has started.""" 121 status = self._get_update_engine_status() 122 if status is None: 123 return False 124 return status[self._CURRENT_OP] in ( 125 self._UPDATE_STATUS_DOWNLOADING, self._UPDATE_STATUS_FINALIZING) 126 127 128 def _has_progress_stopped(self): 129 """Checks that the update_engine progress has stopped moving.""" 130 before = self._get_update_engine_status()[self._PROGRESS] 131 for i in range(0, 10): 132 if before != self._get_update_engine_status()[self._PROGRESS]: 133 return False 134 time.sleep(1) 135 return True 136 137 138 def _get_update_progress(self): 139 """Returns the current payload downloaded progress.""" 140 while True: 141 status = self._get_update_engine_status() 142 if not status: 143 continue 144 if self._is_update_engine_idle(status): 145 err_str = self._get_last_error_string() 146 raise error.TestFail('Update status was idle while trying to ' 147 'get download status. Last error: %s' % 148 err_str) 149 if self._is_update_engine_reporting_error(status): 150 err_str = self._get_last_error_string() 151 raise error.TestFail('Update status reported error: %s' % 152 err_str) 153 # When the update has moved to the final stages, update engine 154 # displays progress as 0.0 but for our needs we will return 1.0 155 if self._is_update_finished_downloading(status): 156 return 1.0 157 # If we call this right after reboot it may not be downloading yet. 158 if status[self._CURRENT_OP] != self._UPDATE_STATUS_DOWNLOADING: 159 time.sleep(1) 160 continue 161 return float(status[self._PROGRESS]) 162 163 164 def _wait_for_update_to_fail(self): 165 """Waits for the update to retry until failure.""" 166 timeout_minutes = 20 167 timeout = time.time() + 60 * timeout_minutes 168 while True: 169 if self._check_update_engine_log_for_entry('Reached max attempts ', 170 raise_error=False): 171 logging.debug('Found log entry for failed update.') 172 if self._is_update_engine_idle(): 173 break 174 time.sleep(1) 175 self._get_update_engine_status() 176 if time.time() > timeout: 177 raise error.TestFail('Update did not fail as expected. Timeout' 178 ': %d minutes.' % timeout_minutes) 179 180 181 def _wait_for_update_to_complete(self, check_kernel_after_update=True): 182 """ 183 Wait for update status to reach NEED_REBOOT. 184 185 @param check_kernel_after_update: True to also check kernel state after 186 the update. 187 188 """ 189 self._wait_for_update_status(self._UPDATE_STATUS_UPDATED_NEED_REBOOT) 190 if check_kernel_after_update: 191 kernel_utils.verify_kernel_state_after_update( 192 self._host if hasattr(self, '_host') else None) 193 194 195 def _wait_for_update_status(self, status_to_wait_for): 196 """ 197 Wait for the update to reach a certain status. 198 199 @param status_to_wait_for: a string of the update status to wait for. 200 201 """ 202 while True: 203 status = self._get_update_engine_status() 204 205 # During reboot, status will be None 206 if status is not None: 207 if self._is_update_engine_reporting_error(status): 208 err_str = self._get_last_error_string() 209 raise error.TestFail('Update status reported error: %s' % 210 err_str) 211 if self._is_update_engine_idle(status): 212 err_str = self._get_last_error_string() 213 raise error.TestFail('Update status was unexpectedly ' 214 'IDLE when we were waiting for the ' 215 'update to complete: %s' % err_str) 216 if status[self._CURRENT_OP] == status_to_wait_for: 217 break 218 time.sleep(1) 219 220 221 def _get_update_engine_status(self, timeout=3600, ignore_timeout=True): 222 """ 223 Gets a dictionary version of update_engine_client --status. 224 225 @param timeout: How long to wait for the status to return. 226 @param ignore_timeout: True to throw an exception if timeout occurs. 227 228 @return Dictionary of values within update_engine_client --status. 229 @raise: error.AutoservError if command times out 230 231 """ 232 status = self._run([self._UPDATE_ENGINE_CLIENT_CMD, '--status'], 233 timeout=timeout, ignore_status=True, 234 ignore_timeout=ignore_timeout) 235 236 if status is None: 237 return None 238 logging.info(status) 239 if status.exit_status != 0: 240 return None 241 status_dict = {} 242 for line in status.stdout.splitlines(): 243 entry = line.partition('=') 244 status_dict[entry[0]] = entry[2] 245 return status_dict 246 247 248 def _check_update_engine_log_for_entry(self, entry, raise_error=False, 249 err_str=None, 250 update_engine_log=None): 251 """ 252 Checks for entries in the update_engine log. 253 254 @param entry: String or tuple of strings to search for. 255 @param raise_error: Fails tests if log doesn't contain entry. 256 @param err_str: The error string to raise if we cannot find entry. 257 @param update_engine_log: Update engine log string you want to 258 search. If None, we will read from the 259 current update engine log. 260 261 @return Boolean if the update engine log contains the entry. 262 263 """ 264 if isinstance(entry, str): 265 # Create a tuple of strings so we can iterate over it. 266 entry = (entry,) 267 268 if not update_engine_log: 269 update_engine_log = self._get_update_engine_log() 270 271 if all(msg in update_engine_log for msg in entry): 272 return True 273 274 if not raise_error: 275 return False 276 277 error_str = ('Did not find expected string(s) in update_engine log: ' 278 '%s' % entry) 279 logging.debug(error_str) 280 raise error.TestFail(err_str if err_str else error_str) 281 282 283 def _is_update_finished_downloading(self, status=None): 284 """ 285 Checks if the update has moved to the final stages. 286 287 @param status: Output of _get_update_engine_status(). If None that 288 function will be called here first. 289 290 """ 291 if status is None: 292 status = self._get_update_engine_status() 293 return status[self._CURRENT_OP] in [ 294 self._UPDATE_STATUS_FINALIZING, 295 self._UPDATE_STATUS_UPDATED_NEED_REBOOT] 296 297 298 def _is_update_engine_idle(self, status=None): 299 """ 300 Checks if the update engine is idle. 301 302 @param status: Output of _get_update_engine_status(). If None that 303 function will be called here first. 304 305 """ 306 if status is None: 307 status = self._get_update_engine_status() 308 return status[self._CURRENT_OP] == self._UPDATE_STATUS_IDLE 309 310 311 def _is_checking_for_update(self, status=None): 312 """ 313 Checks if the update status is still checking for an update. 314 315 @param status: Output of _get_update_engine_status(). If None that 316 function will be called here first. 317 318 """ 319 if status is None: 320 status = self._get_update_engine_status() 321 return status[self._CURRENT_OP] in ( 322 self._UPDATE_STATUS_CHECKING_FOR_UPDATE, 323 self._UPDATE_STATUS_UPDATE_AVAILABLE) 324 325 326 def _is_update_engine_reporting_error(self, status=None): 327 """ 328 Checks if the update engine status reported an error. 329 330 @param status: Output of _get_update_engine_status(). If None that 331 function will be called here first. 332 333 """ 334 if status is None: 335 status = self._get_update_engine_status() 336 return (status[self._CURRENT_OP] == 337 self._UPDATE_STATUS_REPORTING_ERROR_EVENT) 338 339 340 def _update_continued_where_it_left_off(self, progress, 341 reboot_interrupt=False): 342 """ 343 Checks that the update did not restart after an interruption. 344 345 When testing a reboot interrupt we can do additional checks on the 346 logs before and after reboot to see if the update resumed. 347 348 @param progress: The progress the last time we checked. 349 @param reboot_interrupt: True if we are doing a reboot interrupt test. 350 351 @returns True if update continued. False if update restarted. 352 353 """ 354 completed = self._get_update_progress() 355 logging.info('New value: %f, old value: %f', completed, progress) 356 if completed >= progress: 357 return True 358 359 # Sometimes update_engine will continue an update but the first reported 360 # progress won't be correct. So check the logs for resume info. 361 if not reboot_interrupt or not self._check_update_engine_log_for_entry( 362 'Resuming an update that was previously started'): 363 return False 364 365 # Find the reported Completed and Resumed progress. 366 pattern = ('(.*)/(.*) operations \((.*)%\), (.*)/(.*) bytes downloaded' 367 ' \((.*)%\), overall progress (.*)%') 368 before_pattern = 'Completed %s' % pattern 369 before_log = self._get_update_engine_log(r_index=1) 370 before_match = re.findall(before_pattern, before_log)[-1] 371 after_pattern = 'Resuming after %s' % pattern 372 after_log = self._get_update_engine_log(r_index=0) 373 after_match = re.findall(after_pattern, after_log)[0] 374 logging.debug('Progress before interrupt: %s', before_match) 375 logging.debug('Progress after interrupt: %s', after_match) 376 377 # Check the Resuming progress is greater than Completed progress. 378 for i in range(0, len(before_match)): 379 logging.debug('Comparing %d and %d', int(before_match[i]), 380 int(after_match[i])) 381 if int(before_match[i]) > int(after_match[i]): 382 return False 383 return True 384 385 386 def _append_query_to_url(self, url, query_dict): 387 """ 388 Appends the dictionary kwargs to the URL url as query string. 389 390 This function will replace the already existing query strings in url 391 with the ones in the input dictionary. I also removes keys that have 392 a None value. 393 394 @param url: The given input URL. 395 @param query_dicl: A dictionary of key/values to be converted to query 396 string. 397 @return: The same input URL url but with new query string items added. 398 399 """ 400 # TODO(ahassani): This doesn't work (or maybe should not) for queries 401 # with multiple values for a specific key. 402 parsed_url = list(urlparse.urlsplit(url)) 403 parsed_query = urlparse.parse_qs(parsed_url[3]) 404 for k, v in query_dict.items(): 405 parsed_query[k] = [v] 406 parsed_url[3] = '&'.join( 407 '%s=%s' % (k, v[0]) for k, v in parsed_query.items() 408 if v[0] is not None) 409 return urlparse.urlunsplit(parsed_url) 410 411 412 def _check_for_update(self, update_url, interactive=True, 413 wait_for_completion=False, 414 check_kernel_after_update=True, **kwargs): 415 """ 416 Starts a background update check. 417 418 @param update_url: The URL to get an update from. 419 @param interactive: True if we are doing an interactive update. 420 @param wait_for_completion: True for --update, False for 421 --check_for_update. 422 @param check_kernel_after_update: True to check kernel state after a 423 successful update. False to skip. wait_for_completion must also 424 be True. 425 @param kwargs: The dictionary to be converted to a query string and 426 appended to the end of the update URL. e.g: 427 {'critical_update': True, 'foo': 'bar'} -> 428 'http:/127.0.0.1:8080/update?critical_update=True&foo=bar' Look 429 at nebraska.py or devserver.py for the list of accepted 430 values. If there was already query string in update_url, it will 431 append the new values and override the old ones. 432 433 """ 434 update_url = self._append_query_to_url(update_url, kwargs) 435 cmd = [self._UPDATE_ENGINE_CLIENT_CMD, 436 '--update' if wait_for_completion else '--check_for_update', 437 '--omaha_url=%s' % update_url] 438 439 if not interactive: 440 cmd.append('--interactive=false') 441 self._run(cmd, ignore_status=False) 442 if wait_for_completion and check_kernel_after_update: 443 kernel_utils.verify_kernel_state_after_update( 444 self._host if hasattr(self, '_host') else None) 445 446 447 def _rollback(self, powerwash=False): 448 """ 449 Perform a rollback of rootfs. 450 451 @param powerwash: True to powerwash along with the rollback. 452 453 """ 454 cmd = [self._UPDATE_ENGINE_CLIENT_CMD, '--rollback', '--follow'] 455 if not powerwash: 456 cmd.append('--nopowerwash') 457 logging.info('Performing rollback with cmd: %s.', cmd) 458 self._run(cmd) 459 kernel_utils.verify_kernel_state_after_update(self._host) 460 461 462 def _restart_update_engine(self, ignore_status=False): 463 """ 464 Restarts update-engine. 465 466 @param ignore_status: True to not raise exception on command failure. 467 468 """ 469 self._run(['restart', 'update-engine'], ignore_status=ignore_status) 470 471 472 def _save_extra_update_engine_logs(self, number_of_logs): 473 """ 474 Get the last X number of update_engine logs on the DUT. 475 476 @param number_of_logs: The number of logs to save. 477 478 """ 479 files = self._get_update_engine_logs() 480 481 for i in range(number_of_logs if number_of_logs <= len(files) else 482 len(files)): 483 file = os.path.join(self._UPDATE_ENGINE_LOG_DIR, files[i]) 484 self._get_file(file, self.resultsdir) 485 486 487 def _get_update_engine_logs(self, timeout=3600, ignore_timeout=True): 488 """ 489 Helper function to return the list of files in /var/log/update_engine/. 490 491 @param timeout: How many seconds to wait for command to complete. 492 @param ignore_timeout: True if we should not throw an error on timeout. 493 494 """ 495 cmd = ['ls', '-t', '-1', self._UPDATE_ENGINE_LOG_DIR] 496 return self._run(cmd, timeout=timeout, 497 ignore_timeout=ignore_timeout).stdout.splitlines() 498 499 500 def _get_update_engine_log(self, r_index=0, timeout=3600, 501 ignore_timeout=True): 502 """ 503 Returns the last r_index'th update_engine log. 504 505 @param r_index: The index of the last r_index'th update_engine log 506 in order they were created. For example: 507 0 -> last one. 508 1 -> second to last one. 509 @param timeout: How many seconds to wait for command to complete. 510 @param ignore_timeout: True if we should not throw an error on timeout. 511 512 """ 513 files = self._get_update_engine_logs() 514 log_file = os.path.join(self._UPDATE_ENGINE_LOG_DIR, files[r_index]) 515 return self._run(['cat', log_file]).stdout 516 517 518 def _create_custom_lsb_release(self, update_url, build='0.0.0.0', **kwargs): 519 """ 520 Create a custom lsb-release file. 521 522 In order to tell OOBE to ping a different update server than the 523 default we need to create our own lsb-release. We will include a 524 deserver update URL. 525 526 @param update_url: String of url to use for update check. 527 @param build: String of the build number to use. Represents the 528 Chrome OS build this device thinks it is on. 529 @param kwargs: A dictionary of key/values to be made into a query string 530 and appended to the update_url 531 532 """ 533 update_url = self._append_query_to_url(update_url, kwargs) 534 535 self._run(['mkdir', os.path.dirname(self._CUSTOM_LSB_RELEASE)], 536 ignore_status=True) 537 self._run(['touch', self._CUSTOM_LSB_RELEASE]) 538 self._run(['echo', 'CHROMEOS_RELEASE_VERSION=%s' % build, '>', 539 self._CUSTOM_LSB_RELEASE]) 540 self._run(['echo', 'CHROMEOS_AUSERVER=%s' % update_url, '>>', 541 self._CUSTOM_LSB_RELEASE]) 542 543 544 def _clear_custom_lsb_release(self): 545 """ 546 Delete the custom release file, if any. 547 548 Intended to clear work done by _create_custom_lsb_release(). 549 550 """ 551 self._run(['rm', self._CUSTOM_LSB_RELEASE], ignore_status=True) 552 553 554 def _remove_update_engine_pref(self, pref): 555 """ 556 Delete an update_engine pref file. 557 558 @param pref: The pref file to delete 559 560 """ 561 pref_file = os.path.join(self._UPDATE_ENGINE_PREFS_DIR, pref) 562 self._run(['rm', pref_file], ignore_status=True) 563 564 565 def _get_update_requests(self): 566 """ 567 Get the contents of all the update requests from the most recent log. 568 569 @returns: a sequential list of <request> xml blocks or None if none. 570 571 """ 572 update_log = self._get_update_engine_log() 573 574 # Matches <request ... /request>. The match can be on multiple 575 # lines and the search is not greedy so it only matches one block. 576 return re.findall(r'<request.*?/request>', update_log, re.DOTALL) 577 578 579 def _get_time_of_last_update_request(self): 580 """ 581 Get the time of the last update request from most recent logfile. 582 583 @returns: seconds since epoch of when last update request happened 584 (second accuracy), or None if no such timestamp exists. 585 586 """ 587 update_log = self._get_update_engine_log() 588 589 # Matches any single line with "MMDD/HHMMSS ... Request ... xml", e.g. 590 # "[0723/133526:INFO:omaha_request_action.cc(794)] Request: <?xml". 591 result = re.findall(r'([0-9]{4}/[0-9]{6}).*Request.*xml', update_log) 592 if not result: 593 return None 594 595 LOG_TIMESTAMP_FORMAT = '%m%d/%H%M%S' 596 match = result[-1] 597 598 # The log does not include the year, so set it as this year. 599 # This assumption could cause incorrect behavior, but is unlikely to. 600 current_year = datetime.datetime.now().year 601 log_datetime = datetime.datetime.strptime(match, LOG_TIMESTAMP_FORMAT) 602 log_datetime = log_datetime.replace(year=current_year) 603 604 return time.mktime(log_datetime.timetuple()) 605 606 607 def _take_screenshot(self, filename): 608 """ 609 Take a screenshot and save in resultsdir. 610 611 @param filename: The name of the file to save 612 613 """ 614 try: 615 file_location = os.path.join('/tmp', filename) 616 self._run(['screenshot', file_location]) 617 self._get_file(file_location, self.resultsdir) 618 except (error.AutoservRunError, error.CmdError): 619 logging.exception('Failed to take screenshot.') 620 621 622 def _remove_screenshots(self): 623 """Remove screenshots taken by interrupt tests.""" 624 for file in [self._BEFORE_INTERRUPT_FILENAME, 625 self._AFTER_INTERRUPT_FILENAME]: 626 file_location = os.path.join(self.resultsdir, file) 627 if os.path.exists(file_location): 628 try: 629 os.remove(file_location) 630 except Exception as e: 631 logging.exception('Failed to remove %s', file_location) 632 633 634 def _get_last_error_string(self): 635 """ 636 Gets the last error message in the update engine log. 637 638 @returns: The error message. 639 640 """ 641 err_str = 'Updating payload state for error code: ' 642 log = self._get_update_engine_log().splitlines() 643 targets = [line for line in log if err_str in line] 644 logging.debug('Error lines found: %s', targets) 645 if not targets: 646 return None 647 else: 648 return targets[-1].rpartition(err_str)[2] 649 650 651 def _get_latest_initial_request(self): 652 """ 653 Return the most recent initial update request. 654 655 AU requests occur in a chain of messages back and forth, e.g. the 656 initial request for an update -> the reply with the update -> the 657 report that install has started -> report that install has finished, 658 etc. This function finds the first request in the latest such chain. 659 660 This message has no eventtype listed, or is rebooted_after_update 661 type (as an artifact from a previous update since this one). 662 Subsequent messages in the chain have different eventtype values. 663 664 @returns: string of the entire update request or None. 665 666 """ 667 requests = self._get_update_requests() 668 if not requests: 669 return None 670 671 MATCH_STR = r'eventtype="(.*?)"' 672 for i in xrange(len(requests) - 1, -1, -1): 673 search = re.search(MATCH_STR, requests[i]) 674 if (not search or 675 (search.group(1) == 676 str(update_engine_event.EVENT_TYPE_REBOOTED_AFTER_UPDATE))): 677 return requests[i] 678 679 return None 680