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