devserver: Support update payload metadata for full_payload artifact

BUG=chromium:403086
TEST=devserver_integration_test.py

Change-Id: Iad49f87c9ed79327f1eb5f4b03648f28110f4f15
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/1826048
Tested-by: Amin Hassani <ahassani@chromium.org>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
Reviewed-by: Achuith Bhandarkar <achuith@chromium.org>
Commit-Queue: Amin Hassani <ahassani@chromium.org>
diff --git a/build_artifact.py b/build_artifact.py
index f5c0148..fb62773 100755
--- a/build_artifact.py
+++ b/build_artifact.py
@@ -369,24 +369,6 @@
     return str(self)
 
 
-class AUTestPayload(Artifact):
-  """Wrapper for AUTest delta payloads which need additional setup."""
-
-  def _Setup(self):
-    super(AUTestPayload, self)._Setup()
-
-    # Rename to update.gz.
-    install_path = os.path.join(self.install_dir, self.install_subdir,
-                                self.name)
-    new_install_path = os.path.join(self.install_dir, self.install_subdir,
-                                    devserver_constants.UPDATE_FILE)
-    shutil.move(install_path, new_install_path)
-
-    # Reflect the rename in the list of installed files.
-    self.installed_files.remove(install_path)
-    self.installed_files = [new_install_path]
-
-
 class MultiArtifact(Artifact):
   """Wrapper for artifacts where name matches multiple items.."""
 
@@ -410,6 +392,31 @@
                                          name) for name in self.name]
 
 
+class AUTestPayload(MultiArtifact):
+  """Wrapper for AUTest delta payloads which need additional setup."""
+
+  def _Setup(self):
+    super(AUTestPayload, self)._Setup()
+
+    # Rename to update.gz.
+    # TODO(crbug.com/1008058): Change the devserver such that this renaming is
+    # not needed anymore.
+    for name in self.name:
+      dest_name = (devserver_constants.UPDATE_METADATA_FILE
+                   if name.endswith('.json')
+                   else devserver_constants.UPDATE_FILE)
+
+      install_path = os.path.join(self.install_dir, self.install_subdir, name)
+      new_install_path = os.path.join(self.install_dir, self.install_subdir,
+                                      dest_name)
+      self._Log('moving %s to %s', install_path, new_install_path)
+      shutil.move(install_path, new_install_path)
+
+      # Reflect the rename in the list of installed files.
+      self.installed_files.remove(install_path)
+      self.installed_files.append(new_install_path)
+
+
 class DeltaPayloadBase(AUTestPayload):
   """Delta payloads from the archive_url.
 
@@ -615,7 +622,8 @@
   chromeos_artifact_map.setdefault(tag, []).append(artifact)
 
 
-_AddCrOSArtifact(artifact_info.FULL_PAYLOAD, AUTestPayload, '*_full_*bin')
+_AddCrOSArtifact(artifact_info.FULL_PAYLOAD, AUTestPayload,
+                 '.*full.*bin(\.json)?\\Z', is_regex_name=True)
 
 
 class DeltaPayloadNtoN(DeltaPayloadBase):
diff --git a/devserver_constants.py b/devserver_constants.py
index 70e8c2a..6f99cd0 100644
--- a/devserver_constants.py
+++ b/devserver_constants.py
@@ -46,6 +46,7 @@
 METADATA_HASH_FILE = 'metadata_hash'
 STATEFUL_FILE = 'stateful.tgz'
 UPDATE_FILE = 'update.gz'
+UPDATE_METADATA_FILE = 'update.gz.json'
 
 #### Android files
 ANDROID_BOOT_IMAGE_FILE = 'boot.img'
diff --git a/devserver_integration_test.py b/devserver_integration_test.py
index 3971f75..6209ab2 100755
--- a/devserver_integration_test.py
+++ b/devserver_integration_test.py
@@ -15,10 +15,7 @@
 
 from __future__ import print_function
 
-import cros_update_progress
-import devserver_constants
 import json
-from xml.dom import minidom
 import os
 import psutil
 import shutil
@@ -30,24 +27,31 @@
 import unittest
 import urllib2
 
+from string import Template
+
+from xml.dom import minidom
+
+import cros_update_progress
+import devserver_constants
+
 from chromite.lib import cros_logging as logging
 
 
 # Paths are relative to this script's base directory.
 LABEL = 'devserver'
 TEST_IMAGE_PATH = 'testdata/devserver'
-TEST_IMAGE_NAME = 'update.gz'
-EXPECTED_HASH = 'kGcOinJ0vA8vdYX53FN0F5BdwfY='
+TEST_UPDATE_PAYLOAD_NAME = 'update.gz'
+TEST_UPDATE_PAYLOAD_METADATA_NAME = 'update.gz.json'
 
 # Update request based on Omaha v3 protocol format.
-UPDATE_REQUEST = """<?xml version="1.0" encoding="UTF-8"?>
+UPDATE_REQUEST = Template("""<?xml version="1.0" encoding="UTF-8"?>
 <request protocol="3.0" updater="ChromeOSUpdateEngine" updaterversion="0.1.0.0" ismachine="1">
     <os version="Indy" platform="Chrome OS" sp="0.11.254.2011_03_09_1814_i686"></os>
-    <app appid="{DEV-BUILD}" version="11.254.2011_03_09_1814" lang="en-US" track="developer-build" board="x86-generic" hardware_class="BETA DVT" delta_okay="true">
+    <app appid="$appid" version="11.254.2011_03_09_1814" lang="en-US" track="developer-build" board="x86-generic" hardware_class="BETA DVT" delta_okay="true">
         <updatecheck></updatecheck>
     </app>
 </request>
-"""
+""")
 
 # RPC constants.
 STAGE = 'stage'
@@ -82,14 +86,11 @@
     self.test_data_path = tempfile.mkdtemp()
     self.src_dir = os.path.dirname(__file__)
 
-    # Current location of testdata payload.
-    image_src = os.path.join(self.src_dir, TEST_IMAGE_PATH, TEST_IMAGE_NAME)
-
     # Copy the payload to the location of the update label.
-    self._CreateLabelAndCopyImage(LABEL, image_src)
+    self._CreateLabelAndCopyUpdatePayloadFiles(LABEL)
 
     # Copy the payload to the location of forced label.
-    self._CreateLabelAndCopyImage(API_SET_UPDATE_REQUEST, image_src)
+    self._CreateLabelAndCopyUpdatePayloadFiles(API_SET_UPDATE_REQUEST)
 
     # Allocate temporary files for various devserver outputs.
     self.pidfile = self._MakeTempFile('pid')
@@ -111,11 +112,13 @@
 
   # Helper methods begin here.
 
-  def _CreateLabelAndCopyImage(self, label, image):
+  def _CreateLabelAndCopyUpdatePayloadFiles(self, label):
     """Creates a label location and copies an image to it."""
+    update_dir = os.path.join(self.src_dir, TEST_IMAGE_PATH)
     label_dir = os.path.join(self.test_data_path, label)
     os.makedirs(label_dir)
-    shutil.copy(image, os.path.join(label_dir, TEST_IMAGE_NAME))
+    for name in (TEST_UPDATE_PAYLOAD_NAME, TEST_UPDATE_PAYLOAD_METADATA_NAME):
+      shutil.copy(os.path.join(update_dir, name), label_dir)
 
   def _MakeTempFile(self, suffix):
     """Return path of a newly created temporary file."""
@@ -186,7 +189,8 @@
     # Retrieve PID.
     self.pid = self._ReadIntValueFromFile(self.pidfile, 'pidfile')
 
-  def VerifyHandleUpdate(self, label, use_test_payload=True):
+  def VerifyHandleUpdate(self, label, use_test_payload=True,
+                         appid='{DEV-BUILD}'):
     """Verifies that we can send an update request to the devserver.
 
     This method verifies (using a fake update_request blob) that the devserver
@@ -202,16 +206,15 @@
       url of the update payload if we verified the update.
     """
     update_label = '/'.join([UPDATE, label])
-    response = self._MakeRPC(update_label, data=UPDATE_REQUEST)
+    response = self._MakeRPC(
+        update_label, data=UPDATE_REQUEST.substitute({'appid': appid}))
     self.assertNotEqual('', response)
 
     # Parse the response and check if it contains the right result.
     dom = minidom.parseString(response)
     update = dom.getElementsByTagName('updatecheck')[0]
     expected_static_url = '/'.join([self.devserver_url, STATIC, label])
-    expected_hash = EXPECTED_HASH if use_test_payload else None
-    url = self.VerifyV3Response(update, expected_static_url,
-                                expected_hash=expected_hash)
+    url = self.VerifyV3Response(update, expected_static_url)
 
     # Verify the image we download is correct since we already know what it is.
     if use_test_payload:
@@ -222,7 +225,7 @@
 
     return url
 
-  def VerifyV3Response(self, update, expected_static_url, expected_hash):
+  def VerifyV3Response(self, update, expected_static_url):
     """Verifies the update DOM from a v3 response and returns the url."""
     # Parse the response and check if it contains the right result.
     urls = update.getElementsByTagName('urls')[0]
@@ -236,11 +239,7 @@
     packages = manifest.getElementsByTagName('packages')[0]
     package = packages.getElementsByTagName('package')[0]
     filename = package.getAttribute('name')
-    self.assertEqual(TEST_IMAGE_NAME, filename)
-
-    if expected_hash:
-      hash_value = package.getAttribute('hash')
-      self.assertEqual(EXPECTED_HASH, hash_value)
+    self.assertEqual(TEST_UPDATE_PAYLOAD_NAME, filename)
 
     url = os.path.join(static_url, filename)
     return url
@@ -362,27 +361,31 @@
     This test verifies all the local xbuddy logic by creating a new local folder
     with the necessary update items and verifies we can use all of them.
     """
-    update_data = 'TEST UPDATE'
-    image_data = 'TEST IMAGE'
-    stateful_data = 'STATEFUL STUFFS'
     build_id = 'x86-generic/R32-9999.0.0-a1'
     xbuddy_path = 'x86-generic/R32-9999.0.0-a1/test'
     build_dir = os.path.join(self.test_data_path, build_id)
     os.makedirs(build_dir)
+
+    # Writing dummy files.
+    image_data = 'TEST IMAGE'
     test_image_file = os.path.join(build_dir,
                                    devserver_constants.TEST_IMAGE_FILE)
-    update_file = os.path.join(build_dir, devserver_constants.UPDATE_FILE)
+    with open(test_image_file, 'w') as f:
+      f.write(image_data)
+
+    stateful_data = 'STATEFUL STUFFS'
     stateful_file = os.path.join(build_dir, devserver_constants.STATEFUL_FILE)
+    with open(stateful_file, 'w') as f:
+      f.write(stateful_data)
 
-    logging.info('Creating dummy files')
+    update_dir = os.path.join(self.src_dir, TEST_IMAGE_PATH)
+    for name in (TEST_UPDATE_PAYLOAD_NAME, TEST_UPDATE_PAYLOAD_METADATA_NAME):
+      shutil.copy(os.path.join(update_dir, name), build_dir)
+    with open(os.path.join(build_dir, TEST_UPDATE_PAYLOAD_NAME), 'r') as f:
+      update_data = f.read()
 
-    for item, filename, data in zip(
-        ['full_payload', 'test', 'stateful'],
-        [update_file, test_image_file, stateful_file],
-        [update_data, image_data, stateful_data]):
-      logging.info('Creating file %s', filename)
-      with open(filename, 'w') as fh:
-        fh.write(data)
+    for item, data in zip(['full_payload', 'test', 'stateful'],
+                          [update_data, image_data, stateful_data]):
 
       xbuddy_path = '/'.join([build_id, item])
       logging.info('Testing xbuddy path %s', xbuddy_path)
@@ -472,7 +475,7 @@
 
   def testStageAndUpdate(self):
     """Tests core stage/update autotest workflow where with a test payload."""
-    build_id = 'eve-release/R69-10782.0.0'
+    build_id = 'eve-release/R78-12499.0.0'
     archive_url = 'gs://chromeos-image-archive/%s' % build_id
 
     response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
@@ -493,10 +496,13 @@
     self.assertTrue(os.path.exists(
         os.path.join(staged_dir, devserver_constants.UPDATE_FILE)))
     self.assertTrue(os.path.exists(
+        os.path.join(staged_dir, devserver_constants.UPDATE_METADATA_FILE)))
+    self.assertTrue(os.path.exists(
         os.path.join(staged_dir, devserver_constants.STATEFUL_FILE)))
 
     logging.info('Verifying we can update using the stage update artifacts.')
-    self.VerifyHandleUpdate(build_id, use_test_payload=False)
+    self.VerifyHandleUpdate(build_id, use_test_payload=False,
+                            appid='{01906EA2-3EB2-41F1-8F62-F0B7120EFD2E}')
 
   @unittest.skip('crbug.com/640063 Broken test.')
   def testStageAutotestAndGetPackages(self):
diff --git a/testdata/devserver/update.gz.json b/testdata/devserver/update.gz.json
new file mode 100644
index 0000000..6d37731
--- /dev/null
+++ b/testdata/devserver/update.gz.json
@@ -0,0 +1 @@
+{"appid": "", "is_delta": true, "metadata_signature": null, "metadata_size": 667655, "sha256_hex": "vUQYNjzC5GYnKbomw2/S3XwhrIdhIo1xTp1IilsKGmw=", "size": 46459808, "source_version": "", "target_version": "99999.0.0", "version": 2}