Process DLC update events and generate hostlogs

If DLCs are updated along with the platform update, there will be
multiple app sections per event in the update_engine log - one for the
platform update, and one for each DLC being updated. We should extract
these events so we can check if DLCs are being updated correctly.

BUG=chromium:1077166
TEST=autoupdate_WithDLC (WIP test, see crrev.com/c/2234009), non-DLC
tests to make sure they still work (autoupdate_FromUI.full,
autoupdate_ForcedOOBEUpdate.interrupt.reboot.full)

Change-Id: Idf85f33d66842b1c4e43bdf3872d1384d0337cd6
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2248915
Tested-by: Kyle Shimabukuro <kyleshima@chromium.org>
Reviewed-by: David Haddock <dhaddock@chromium.org>
Commit-Queue: David Haddock <dhaddock@chromium.org>
diff --git a/server/cros/update_engine/update_engine_test.py b/server/cros/update_engine/update_engine_test.py
index 682a7e6..dc22593 100644
--- a/server/cros/update_engine/update_engine_test.py
+++ b/server/cros/update_engine/update_engine_test.py
@@ -391,12 +391,16 @@
 
     # TODO(ahassani): Move this to chromite so it can be used by endtoend tests
     # too so we don't have to rely on request_log API on nebraska.
-    def _extract_request_logs(self, update_engine_log):
+    def _extract_request_logs(self, update_engine_log, is_dlc=False):
         """
         Extracts request logs from an update_engine log.
 
         @param update_engine_log: The update_engine log as a string.
-        @returns a list object representing the request logs.
+        @param is_dlc: True to return the request logs for the DLC updates
+                       instead of the platform update.
+        @returns a list object representing the platform (OS) request logs, or
+                 a dictionary of lists representing DLC request logs,
+                 keyed by DLC ID, if is_dlc is True.
 
         """
         # Looking for all request XML strings in the log.
@@ -424,28 +428,77 @@
                                  'log file.')
 
         result = []
+        dlc_results = {}
         for timestamp, request in zip(timestamps, requests):
 
             root = ElementTree.fromstring(request)
-            app = root.find('app')
-            event = app.find('event')
 
-            result.append({
-                'version': app.attrib.get('version'),
-                'event_type': (int(event.attrib.get('eventtype'))
-                              if event is not None else None),
-                'event_result': (int(event.attrib.get('eventresult'))
-                                if event is not None else None),
-                'timestamp': timestamp.strftime(self._TIMESTAMP_FORMAT),
-            })
+            # There may be events for multiple apps if DLCs are installed.
+            # See below (trimmed) example request including DLC:
+            #
+            # <request requestid=...>
+            #   <os version="Indy" platform=...></os>
+            #   <app appid="{DB5199C7-358B-4E1F-B4F6-AF6D2DD01A38}"
+            #       version="13265.0.0" track=...>
+            #     <event eventtype="13" eventresult="1"></event>
+            #   </app>
+            #   <app appid="{DB5199C7-358B-4E1F-B4F6-AF6D2DD01A38}_dummy-dlc"
+            #       version="0.0.0.0" track=...>
+            #     <event eventtype="13" eventresult="1"></event>
+            #   </app>
+            # </request>
+            #
+            # The first <app> section is for the platform update. The second
+            # is for the DLC update.
+            #
+            # Example without DLC:
+            # <request requestid=...>
+            #   <os version="Indy" platform=...></os>
+            #   <app appid="{DB5199C7-358B-4E1F-B4F6-AF6D2DD01A38}"
+            #       version="13265.0.0" track=...>
+            #     <event eventtype="13" eventresult="1"></event>
+            #   </app>
+            # </request>
 
-            previous_version = (event.attrib.get('previousversion')
-                                if event is not None else None)
-            if previous_version:
-                result[-1]['previous_version'] = previous_version
+            apps = root.findall('app')
+            for app in apps:
+                event = app.find('event')
 
-        logging.info('Extracted Request log: %s', result)
-        return result
+                event_info = {
+                    'version': app.attrib.get('version'),
+                    'event_type': (int(event.attrib.get('eventtype'))
+                                  if event is not None else None),
+                    'event_result': (int(event.attrib.get('eventresult'))
+                                    if event is not None else None),
+                    'timestamp': timestamp.strftime(self._TIMESTAMP_FORMAT),
+                }
+
+                previous_version = (event.attrib.get('previousversion')
+                                    if event is not None else None)
+                if previous_version:
+                    event_info['previous_version'] = previous_version
+
+                # Check if the event is for the platform update or for a DLC
+                # by checking the appid. For platform, the appid looks like:
+                #     {DB5199C7-358B-4E1F-B4F6-AF6D2DD01A38}
+                # For DLCs, it is the platform app ID + _ + the DLC ID:
+                #     {DB5199C7-358B-4E1F-B4F6-AF6D2DD01A38}_dummy-dlc
+                id_segments = app.attrib.get('appid').split('_')
+                if len(id_segments) > 1:
+                    dlc_id = id_segments[1]
+                    if dlc_id in dlc_results:
+                        dlc_results[dlc_id].append(event_info)
+                    else:
+                        dlc_results[dlc_id] = [event_info]
+                else:
+                    result.append(event_info)
+
+        if is_dlc:
+            logging.info('Extracted DLC request logs: %s', dlc_results)
+            return dlc_results
+        else:
+            logging.info('Extracted platform (OS) request log: %s', result)
+            return result
 
 
     def _create_hostlog_files(self):
@@ -474,6 +527,49 @@
         return rootfs_hostlog, reboot_hostlog
 
 
+    def _create_dlc_hostlog_files(self):
+        """Create the rootfs and reboot hostlog files for DLC updates.
+
+        Each DLC has its own set of update requests in the logs together with
+        the platform update requests. To ensure the DLC update was successful
+        we will compare the update events against the expected events, which
+        are the same expected events as for the platform update. There is a
+        hostlog for the rootfs update and the post-reboot update check for
+        each DLC.
+
+        @returns two dictionaries, one for the rootfs DLC update and one for
+                 the post-reboot check. The keys are DLC IDs and the values
+                 are the hostlog filenames.
+
+        """
+        dlc_rootfs_hostlogs = {}
+        dlc_reboot_hostlogs = {}
+
+        dlc_rootfs_request_logs = self._extract_request_logs(
+            self._get_update_engine_log(1), is_dlc=True)
+
+        for dlc_id in dlc_rootfs_request_logs:
+            dlc_rootfs_hostlog = os.path.join(self.resultsdir,
+                                              'hostlog_' + dlc_id)
+            dlc_rootfs_hostlogs[dlc_id] = dlc_rootfs_hostlog
+            with open(dlc_rootfs_hostlog, 'w') as fp:
+                # Same number of events for DLC updates as for platform
+                json.dump(dlc_rootfs_request_logs[dlc_id][-4:], fp)
+
+        dlc_reboot_request_logs = self._extract_request_logs(
+            self._get_update_engine_log(0), is_dlc=True)
+
+        for dlc_id in dlc_reboot_request_logs:
+            dlc_reboot_hostlog = os.path.join(self.resultsdir,
+                                              'hostlog_' + dlc_id + '_reboot')
+            dlc_reboot_hostlogs[dlc_id] = dlc_reboot_hostlog
+            with open(dlc_reboot_hostlog, 'w') as fp:
+                # Same number of events for DLC updates as for platform
+                json.dump(dlc_reboot_request_logs[dlc_id][:1], fp)
+
+        return dlc_rootfs_hostlogs, dlc_reboot_hostlogs
+
+
     def _set_active_p2p_host(self, host):
         """
         Choose which p2p host device to run commands on.