| # Copyright 2018 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import datetime |
| import logging |
| import os |
| import re |
| import shutil |
| import time |
| import urlparse |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib import utils |
| from autotest_lib.client.common_lib.cros import kernel_utils |
| from autotest_lib.client.cros.update_engine import update_engine_event |
| |
| _DEFAULT_RUN = utils.run |
| _DEFAULT_COPY = shutil.copy |
| |
| class UpdateEngineUtil(object): |
| """ |
| Utility code shared between client and server update_engine autotests. |
| |
| All update_engine autotests inherit from either the client or server |
| version of update_engine_test: |
| client/cros/update_engine/update_engine_test.py |
| server/cros/update_engine/update_engine_test.py |
| |
| These update_engine_test classes inherit from test and update_engine_util. |
| For update_engine_util to work seamlessly, we need the client and server |
| update_engine_tests to define _run() and _get_file() functions: |
| server side: host.run and host.get_file |
| client side: utils.run and shutil.copy |
| |
| """ |
| |
| # Update engine status lines. |
| _PROGRESS = 'PROGRESS' |
| _CURRENT_OP = 'CURRENT_OP' |
| |
| # Source version when we force an update. |
| _FORCED_UPDATE = 'ForcedUpdate' |
| |
| # update_engine_client command |
| _UPDATE_ENGINE_CLIENT_CMD = 'update_engine_client' |
| |
| # Update engine statuses. |
| _UPDATE_STATUS_IDLE = 'UPDATE_STATUS_IDLE' |
| _UPDATE_STATUS_CHECKING_FOR_UPDATE = 'UPDATE_STATUS_CHECKING_FOR_UPDATE' |
| _UPDATE_STATUS_UPDATE_AVAILABLE = 'UPDATE_STATUS_UPDATE_AVAILABLE' |
| _UPDATE_STATUS_DOWNLOADING = 'UPDATE_STATUS_DOWNLOADING' |
| _UPDATE_STATUS_FINALIZING = 'UPDATE_STATUS_FINALIZING' |
| _UPDATE_STATUS_UPDATED_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' |
| _UPDATE_STATUS_REPORTING_ERROR_EVENT = 'UPDATE_STATUS_REPORTING_ERROR_EVENT' |
| |
| # Files used by the tests. |
| _UPDATE_ENGINE_LOG = '/var/log/update_engine.log' |
| _UPDATE_ENGINE_LOG_DIR = '/var/log/update_engine/' |
| _CUSTOM_LSB_RELEASE = '/mnt/stateful_partition/etc/lsb-release' |
| _UPDATE_ENGINE_PREFS_DIR = '/var/lib/update_engine/prefs/' |
| |
| # Update engine prefs |
| _UPDATE_CHECK_RESPONSE_HASH = 'update-check-response-hash' |
| |
| # Interrupt types supported in AU tests. |
| _REBOOT_INTERRUPT = 'reboot' |
| _SUSPEND_INTERRUPT = 'suspend' |
| _NETWORK_INTERRUPT = 'network' |
| _SUPPORTED_INTERRUPTS = [_REBOOT_INTERRUPT, _SUSPEND_INTERRUPT, |
| _NETWORK_INTERRUPT] |
| |
| # Public key used to force update_engine to verify omaha response data on |
| # test images. |
| _IMAGE_PUBLIC_KEY = 'LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZE03Z25kNDNjV2ZRenlydDE2UQpESEUrVDB5eGcxOE9aTys5c2M4aldwakMxekZ0b01Gb2tFU2l1OVRMVXArS1VDMjc0ZitEeElnQWZTQ082VTVECkpGUlBYVXp2ZTF2YVhZZnFsalVCeGMrSlljR2RkNlBDVWw0QXA5ZjAyRGhrckduZi9ya0hPQ0VoRk5wbTUzZG8Kdlo5QTZRNUtCZmNnMUhlUTA4OG9wVmNlUUd0VW1MK2JPTnE1dEx2TkZMVVUwUnUwQW00QURKOFhtdzRycHZxdgptWEphRm1WdWYvR3g3K1RPbmFKdlpUZU9POUFKSzZxNlY4RTcrWlppTUljNUY0RU9zNUFYL2xaZk5PM1JWZ0cyCk83RGh6emErbk96SjNaSkdLNVI0V3daZHVobjlRUllvZ1lQQjBjNjI4NzhxWHBmMkJuM05wVVBpOENmL1JMTU0KbVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==' |
| |
| # Screenshot names used by interrupt tests |
| _BEFORE_INTERRUPT_FILENAME = 'before_interrupt.png' |
| _AFTER_INTERRUPT_FILENAME = 'after_interrupt.png' |
| |
| |
| def __init__(self, run_func=_DEFAULT_RUN, get_file=_DEFAULT_COPY): |
| """ |
| Initialize this class with _run() and _get_file() functions. |
| |
| @param run_func: the function to use to run commands on the client. |
| Defaults for use by client tests, but can be |
| overwritten to run remotely from a server test. |
| @param get_file: the function to use when copying a file. Defaults |
| for use by client tests. Called with |
| (file, destination) syntax. |
| |
| """ |
| self._set_util_functions(run_func, get_file) |
| |
| |
| def _set_util_functions(self, run_func=_DEFAULT_RUN, |
| get_file=_DEFAULT_COPY): |
| """See __init__().""" |
| self._run = run_func |
| self._get_file = get_file |
| |
| |
| def _wait_for_progress(self, progress): |
| """ |
| Waits until we reach the percentage passed as the input. |
| |
| @param progress: The progress [0.0 .. 1.0] we want to wait for. |
| """ |
| while True: |
| completed = self._get_update_progress() |
| logging.debug('Checking if %s is greater than %s', completed, |
| progress) |
| if completed >= progress: |
| break |
| time.sleep(1) |
| |
| |
| def _is_update_started(self): |
| """Checks if the update has started.""" |
| status = self._get_update_engine_status() |
| if status is None: |
| return False |
| return status[self._CURRENT_OP] in ( |
| self._UPDATE_STATUS_DOWNLOADING, self._UPDATE_STATUS_FINALIZING) |
| |
| |
| def _has_progress_stopped(self): |
| """Checks that the update_engine progress has stopped moving.""" |
| before = self._get_update_engine_status()[self._PROGRESS] |
| for i in range(0, 10): |
| if before != self._get_update_engine_status()[self._PROGRESS]: |
| return False |
| time.sleep(1) |
| return True |
| |
| |
| def _get_update_progress(self): |
| """Returns the current payload downloaded progress.""" |
| while True: |
| status = self._get_update_engine_status() |
| if not status: |
| continue |
| if self._is_update_engine_idle(status): |
| err_str = self._get_last_error_string() |
| raise error.TestFail('Update status was idle while trying to ' |
| 'get download status. Last error: %s' % |
| err_str) |
| if self._is_update_engine_reporting_error(status): |
| err_str = self._get_last_error_string() |
| raise error.TestFail('Update status reported error: %s' % |
| err_str) |
| # When the update has moved to the final stages, update engine |
| # displays progress as 0.0 but for our needs we will return 1.0 |
| if self._is_update_finished_downloading(status): |
| return 1.0 |
| # If we call this right after reboot it may not be downloading yet. |
| if status[self._CURRENT_OP] != self._UPDATE_STATUS_DOWNLOADING: |
| time.sleep(1) |
| continue |
| return float(status[self._PROGRESS]) |
| |
| |
| def _wait_for_update_to_fail(self): |
| """Waits for the update to retry until failure.""" |
| timeout_minutes = 20 |
| timeout = time.time() + 60 * timeout_minutes |
| while True: |
| if self._check_update_engine_log_for_entry('Reached max attempts ', |
| raise_error=False): |
| logging.debug('Found log entry for failed update.') |
| if self._is_update_engine_idle(): |
| break |
| time.sleep(1) |
| self._get_update_engine_status() |
| if time.time() > timeout: |
| raise error.TestFail('Update did not fail as expected. Timeout' |
| ': %d minutes.' % timeout_minutes) |
| |
| |
| def _wait_for_update_to_complete(self, check_kernel_after_update=True): |
| """ |
| Wait for update status to reach NEED_REBOOT. |
| |
| @param check_kernel_after_update: True to also check kernel state after |
| the update. |
| |
| """ |
| self._wait_for_update_status(self._UPDATE_STATUS_UPDATED_NEED_REBOOT) |
| if check_kernel_after_update: |
| kernel_utils.verify_kernel_state_after_update( |
| self._host if hasattr(self, '_host') else None) |
| |
| |
| def _wait_for_update_status(self, status_to_wait_for): |
| """ |
| Wait for the update to reach a certain status. |
| |
| @param status_to_wait_for: a string of the update status to wait for. |
| |
| """ |
| while True: |
| status = self._get_update_engine_status() |
| |
| # During reboot, status will be None |
| if status is not None: |
| if self._is_update_engine_reporting_error(status): |
| err_str = self._get_last_error_string() |
| raise error.TestFail('Update status reported error: %s' % |
| err_str) |
| if self._is_update_engine_idle(status): |
| err_str = self._get_last_error_string() |
| raise error.TestFail('Update status was unexpectedly ' |
| 'IDLE when we were waiting for the ' |
| 'update to complete: %s' % err_str) |
| if status[self._CURRENT_OP] == status_to_wait_for: |
| break |
| time.sleep(1) |
| |
| |
| def _get_update_engine_status(self, timeout=3600, ignore_timeout=True): |
| """ |
| Gets a dictionary version of update_engine_client --status. |
| |
| @param timeout: How long to wait for the status to return. |
| @param ignore_timeout: True to throw an exception if timeout occurs. |
| |
| @return Dictionary of values within update_engine_client --status. |
| @raise: error.AutoservError if command times out |
| |
| """ |
| status = self._run([self._UPDATE_ENGINE_CLIENT_CMD, '--status'], |
| timeout=timeout, ignore_status=True, |
| ignore_timeout=ignore_timeout) |
| |
| if status is None: |
| return None |
| logging.info(status) |
| if status.exit_status != 0: |
| return None |
| status_dict = {} |
| for line in status.stdout.splitlines(): |
| entry = line.partition('=') |
| status_dict[entry[0]] = entry[2] |
| return status_dict |
| |
| |
| def _check_update_engine_log_for_entry(self, entry, raise_error=False, |
| err_str=None, |
| update_engine_log=None): |
| """ |
| Checks for entries in the update_engine log. |
| |
| @param entry: String or tuple of strings to search for. |
| @param raise_error: Fails tests if log doesn't contain entry. |
| @param err_str: The error string to raise if we cannot find entry. |
| @param update_engine_log: Update engine log string you want to |
| search. If None, we will read from the |
| current update engine log. |
| |
| @return Boolean if the update engine log contains the entry. |
| |
| """ |
| if isinstance(entry, str): |
| # Create a tuple of strings so we can iterate over it. |
| entry = (entry,) |
| |
| if not update_engine_log: |
| update_engine_log = self._get_update_engine_log() |
| |
| if all(msg in update_engine_log for msg in entry): |
| return True |
| |
| if not raise_error: |
| return False |
| |
| error_str = ('Did not find expected string(s) in update_engine log: ' |
| '%s' % entry) |
| logging.debug(error_str) |
| raise error.TestFail(err_str if err_str else error_str) |
| |
| |
| def _is_update_finished_downloading(self, status=None): |
| """ |
| Checks if the update has moved to the final stages. |
| |
| @param status: Output of _get_update_engine_status(). If None that |
| function will be called here first. |
| |
| """ |
| if status is None: |
| status = self._get_update_engine_status() |
| return status[self._CURRENT_OP] in [ |
| self._UPDATE_STATUS_FINALIZING, |
| self._UPDATE_STATUS_UPDATED_NEED_REBOOT] |
| |
| |
| def _is_update_engine_idle(self, status=None): |
| """ |
| Checks if the update engine is idle. |
| |
| @param status: Output of _get_update_engine_status(). If None that |
| function will be called here first. |
| |
| """ |
| if status is None: |
| status = self._get_update_engine_status() |
| return status[self._CURRENT_OP] == self._UPDATE_STATUS_IDLE |
| |
| |
| def _is_checking_for_update(self, status=None): |
| """ |
| Checks if the update status is still checking for an update. |
| |
| @param status: Output of _get_update_engine_status(). If None that |
| function will be called here first. |
| |
| """ |
| if status is None: |
| status = self._get_update_engine_status() |
| return status[self._CURRENT_OP] in ( |
| self._UPDATE_STATUS_CHECKING_FOR_UPDATE, |
| self._UPDATE_STATUS_UPDATE_AVAILABLE) |
| |
| |
| def _is_update_engine_reporting_error(self, status=None): |
| """ |
| Checks if the update engine status reported an error. |
| |
| @param status: Output of _get_update_engine_status(). If None that |
| function will be called here first. |
| |
| """ |
| if status is None: |
| status = self._get_update_engine_status() |
| return (status[self._CURRENT_OP] == |
| self._UPDATE_STATUS_REPORTING_ERROR_EVENT) |
| |
| |
| def _update_continued_where_it_left_off(self, progress, |
| reboot_interrupt=False): |
| """ |
| Checks that the update did not restart after an interruption. |
| |
| When testing a reboot interrupt we can do additional checks on the |
| logs before and after reboot to see if the update resumed. |
| |
| @param progress: The progress the last time we checked. |
| @param reboot_interrupt: True if we are doing a reboot interrupt test. |
| |
| @returns True if update continued. False if update restarted. |
| |
| """ |
| completed = self._get_update_progress() |
| logging.info('New value: %f, old value: %f', completed, progress) |
| if completed >= progress: |
| return True |
| |
| # Sometimes update_engine will continue an update but the first reported |
| # progress won't be correct. So check the logs for resume info. |
| if not reboot_interrupt or not self._check_update_engine_log_for_entry( |
| 'Resuming an update that was previously started'): |
| return False |
| |
| # Find the reported Completed and Resumed progress. |
| pattern = ('(.*)/(.*) operations \((.*)%\), (.*)/(.*) bytes downloaded' |
| ' \((.*)%\), overall progress (.*)%') |
| before_pattern = 'Completed %s' % pattern |
| before_log = self._get_update_engine_log(r_index=1) |
| before_match = re.findall(before_pattern, before_log)[-1] |
| after_pattern = 'Resuming after %s' % pattern |
| after_log = self._get_update_engine_log(r_index=0) |
| after_match = re.findall(after_pattern, after_log)[0] |
| logging.debug('Progress before interrupt: %s', before_match) |
| logging.debug('Progress after interrupt: %s', after_match) |
| |
| # Check the Resuming progress is greater than Completed progress. |
| for i in range(0, len(before_match)): |
| logging.debug('Comparing %d and %d', int(before_match[i]), |
| int(after_match[i])) |
| if int(before_match[i]) > int(after_match[i]): |
| return False |
| return True |
| |
| |
| def _append_query_to_url(self, url, query_dict): |
| """ |
| Appends the dictionary kwargs to the URL url as query string. |
| |
| This function will replace the already existing query strings in url |
| with the ones in the input dictionary. I also removes keys that have |
| a None value. |
| |
| @param url: The given input URL. |
| @param query_dicl: A dictionary of key/values to be converted to query |
| string. |
| @return: The same input URL url but with new query string items added. |
| |
| """ |
| # TODO(ahassani): This doesn't work (or maybe should not) for queries |
| # with multiple values for a specific key. |
| parsed_url = list(urlparse.urlsplit(url)) |
| parsed_query = urlparse.parse_qs(parsed_url[3]) |
| for k, v in query_dict.items(): |
| parsed_query[k] = [v] |
| parsed_url[3] = '&'.join( |
| '%s=%s' % (k, v[0]) for k, v in parsed_query.items() |
| if v[0] is not None) |
| return urlparse.urlunsplit(parsed_url) |
| |
| |
| def _check_for_update(self, update_url, interactive=True, |
| wait_for_completion=False, |
| check_kernel_after_update=True, **kwargs): |
| """ |
| Starts a background update check. |
| |
| @param update_url: The URL to get an update from. |
| @param interactive: True if we are doing an interactive update. |
| @param wait_for_completion: True for --update, False for |
| --check_for_update. |
| @param check_kernel_after_update: True to check kernel state after a |
| successful update. False to skip. wait_for_completion must also |
| be True. |
| @param kwargs: The dictionary to be converted to a query string and |
| appended to the end of the update URL. e.g: |
| {'critical_update': True, 'foo': 'bar'} -> |
| 'http:/127.0.0.1:8080/update?critical_update=True&foo=bar' Look |
| at nebraska.py or devserver.py for the list of accepted |
| values. If there was already query string in update_url, it will |
| append the new values and override the old ones. |
| |
| """ |
| update_url = self._append_query_to_url(update_url, kwargs) |
| cmd = [self._UPDATE_ENGINE_CLIENT_CMD, |
| '--update' if wait_for_completion else '--check_for_update', |
| '--omaha_url=%s' % update_url] |
| |
| if not interactive: |
| cmd.append('--interactive=false') |
| self._run(cmd, ignore_status=False) |
| if wait_for_completion and check_kernel_after_update: |
| kernel_utils.verify_kernel_state_after_update( |
| self._host if hasattr(self, '_host') else None) |
| |
| |
| def _rollback(self, powerwash=False): |
| """ |
| Perform a rollback of rootfs. |
| |
| @param powerwash: True to powerwash along with the rollback. |
| |
| """ |
| cmd = [self._UPDATE_ENGINE_CLIENT_CMD, '--rollback', '--follow'] |
| if not powerwash: |
| cmd.append('--nopowerwash') |
| logging.info('Performing rollback with cmd: %s.', cmd) |
| self._run(cmd) |
| kernel_utils.verify_kernel_state_after_update(self._host) |
| |
| |
| def _restart_update_engine(self, ignore_status=False): |
| """ |
| Restarts update-engine. |
| |
| @param ignore_status: True to not raise exception on command failure. |
| |
| """ |
| self._run(['restart', 'update-engine'], ignore_status=ignore_status) |
| |
| |
| def _save_extra_update_engine_logs(self, number_of_logs): |
| """ |
| Get the last X number of update_engine logs on the DUT. |
| |
| @param number_of_logs: The number of logs to save. |
| |
| """ |
| files = self._get_update_engine_logs() |
| |
| for i in range(number_of_logs if number_of_logs <= len(files) else |
| len(files)): |
| file = os.path.join(self._UPDATE_ENGINE_LOG_DIR, files[i]) |
| self._get_file(file, self.resultsdir) |
| |
| |
| def _get_update_engine_logs(self, timeout=3600, ignore_timeout=True): |
| """ |
| Helper function to return the list of files in /var/log/update_engine/. |
| |
| @param timeout: How many seconds to wait for command to complete. |
| @param ignore_timeout: True if we should not throw an error on timeout. |
| |
| """ |
| cmd = ['ls', '-t', '-1', self._UPDATE_ENGINE_LOG_DIR] |
| return self._run(cmd, timeout=timeout, |
| ignore_timeout=ignore_timeout).stdout.splitlines() |
| |
| |
| def _get_update_engine_log(self, r_index=0, timeout=3600, |
| ignore_timeout=True): |
| """ |
| Returns the last r_index'th update_engine log. |
| |
| @param r_index: The index of the last r_index'th update_engine log |
| in order they were created. For example: |
| 0 -> last one. |
| 1 -> second to last one. |
| @param timeout: How many seconds to wait for command to complete. |
| @param ignore_timeout: True if we should not throw an error on timeout. |
| |
| """ |
| files = self._get_update_engine_logs() |
| log_file = os.path.join(self._UPDATE_ENGINE_LOG_DIR, files[r_index]) |
| return self._run(['cat', log_file]).stdout |
| |
| |
| def _create_custom_lsb_release(self, update_url, build='0.0.0.0', **kwargs): |
| """ |
| Create a custom lsb-release file. |
| |
| In order to tell OOBE to ping a different update server than the |
| default we need to create our own lsb-release. We will include a |
| deserver update URL. |
| |
| @param update_url: String of url to use for update check. |
| @param build: String of the build number to use. Represents the |
| Chrome OS build this device thinks it is on. |
| @param kwargs: A dictionary of key/values to be made into a query string |
| and appended to the update_url |
| |
| """ |
| update_url = self._append_query_to_url(update_url, kwargs) |
| |
| self._run(['mkdir', os.path.dirname(self._CUSTOM_LSB_RELEASE)], |
| ignore_status=True) |
| self._run(['touch', self._CUSTOM_LSB_RELEASE]) |
| self._run(['echo', 'CHROMEOS_RELEASE_VERSION=%s' % build, '>', |
| self._CUSTOM_LSB_RELEASE]) |
| self._run(['echo', 'CHROMEOS_AUSERVER=%s' % update_url, '>>', |
| self._CUSTOM_LSB_RELEASE]) |
| |
| |
| def _clear_custom_lsb_release(self): |
| """ |
| Delete the custom release file, if any. |
| |
| Intended to clear work done by _create_custom_lsb_release(). |
| |
| """ |
| self._run(['rm', self._CUSTOM_LSB_RELEASE], ignore_status=True) |
| |
| |
| def _remove_update_engine_pref(self, pref): |
| """ |
| Delete an update_engine pref file. |
| |
| @param pref: The pref file to delete |
| |
| """ |
| pref_file = os.path.join(self._UPDATE_ENGINE_PREFS_DIR, pref) |
| self._run(['rm', pref_file], ignore_status=True) |
| |
| |
| def _get_update_requests(self): |
| """ |
| Get the contents of all the update requests from the most recent log. |
| |
| @returns: a sequential list of <request> xml blocks or None if none. |
| |
| """ |
| update_log = self._get_update_engine_log() |
| |
| # Matches <request ... /request>. The match can be on multiple |
| # lines and the search is not greedy so it only matches one block. |
| return re.findall(r'<request.*?/request>', update_log, re.DOTALL) |
| |
| |
| def _get_time_of_last_update_request(self): |
| """ |
| Get the time of the last update request from most recent logfile. |
| |
| @returns: seconds since epoch of when last update request happened |
| (second accuracy), or None if no such timestamp exists. |
| |
| """ |
| update_log = self._get_update_engine_log() |
| |
| # Matches any single line with "MMDD/HHMMSS ... Request ... xml", e.g. |
| # "[0723/133526:INFO:omaha_request_action.cc(794)] Request: <?xml". |
| result = re.findall(r'([0-9]{4}/[0-9]{6}).*Request.*xml', update_log) |
| if not result: |
| return None |
| |
| LOG_TIMESTAMP_FORMAT = '%m%d/%H%M%S' |
| match = result[-1] |
| |
| # The log does not include the year, so set it as this year. |
| # This assumption could cause incorrect behavior, but is unlikely to. |
| current_year = datetime.datetime.now().year |
| log_datetime = datetime.datetime.strptime(match, LOG_TIMESTAMP_FORMAT) |
| log_datetime = log_datetime.replace(year=current_year) |
| |
| return time.mktime(log_datetime.timetuple()) |
| |
| |
| def _take_screenshot(self, filename): |
| """ |
| Take a screenshot and save in resultsdir. |
| |
| @param filename: The name of the file to save |
| |
| """ |
| try: |
| file_location = os.path.join('/tmp', filename) |
| self._run(['screenshot', file_location]) |
| self._get_file(file_location, self.resultsdir) |
| except (error.AutoservRunError, error.CmdError): |
| logging.exception('Failed to take screenshot.') |
| |
| |
| def _remove_screenshots(self): |
| """Remove screenshots taken by interrupt tests.""" |
| for file in [self._BEFORE_INTERRUPT_FILENAME, |
| self._AFTER_INTERRUPT_FILENAME]: |
| file_location = os.path.join(self.resultsdir, file) |
| if os.path.exists(file_location): |
| try: |
| os.remove(file_location) |
| except Exception as e: |
| logging.exception('Failed to remove %s', file_location) |
| |
| |
| def _get_last_error_string(self): |
| """ |
| Gets the last error message in the update engine log. |
| |
| @returns: The error message. |
| |
| """ |
| err_str = 'Updating payload state for error code: ' |
| log = self._get_update_engine_log().splitlines() |
| targets = [line for line in log if err_str in line] |
| logging.debug('Error lines found: %s', targets) |
| if not targets: |
| return None |
| else: |
| return targets[-1].rpartition(err_str)[2] |
| |
| |
| def _get_latest_initial_request(self): |
| """ |
| Return the most recent initial update request. |
| |
| AU requests occur in a chain of messages back and forth, e.g. the |
| initial request for an update -> the reply with the update -> the |
| report that install has started -> report that install has finished, |
| etc. This function finds the first request in the latest such chain. |
| |
| This message has no eventtype listed, or is rebooted_after_update |
| type (as an artifact from a previous update since this one). |
| Subsequent messages in the chain have different eventtype values. |
| |
| @returns: string of the entire update request or None. |
| |
| """ |
| requests = self._get_update_requests() |
| if not requests: |
| return None |
| |
| MATCH_STR = r'eventtype="(.*?)"' |
| for i in xrange(len(requests) - 1, -1, -1): |
| search = re.search(MATCH_STR, requests[i]) |
| if (not search or |
| (search.group(1) == |
| str(update_engine_event.EVENT_TYPE_REBOOTED_AFTER_UPDATE))): |
| return requests[i] |
| |
| return None |