Revert "autoupdate.py: Deprecate autoupdate.py"

This reverts commit 3de3e2bad7d6707a0548ead60ff4ab19f731ce7f.

Reason for revert: It turns out moblab still has this use case and there
are external users use it too. so I guess we have to just suck it up and
put this back.

Original change's description:
> autoupdate.py: Deprecate autoupdate.py
>
> This mechanism is not used in the lab anymore and has been replaced by
> gs_cache/nebraska_wrapper.py in crrev.com/c/2258234.
>
> BUG=chromium:1078188
> TEST=./devserver_integration_test.py
>
> Change-Id: Ifa007df983aab49c808360df6ba0b62b7031221c
> Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2486466
> Tested-by: Amin Hassani <ahassani@chromium.org>
> Commit-Queue: Amin Hassani <ahassani@chromium.org>
> Reviewed-by: Jae Hoon Kim <kimjae@chromium.org>

Bug: chromium:1078188, chromium:1149703
Change-Id: Ic9a1841419de5d4a7f2158e432cdaf3f79b273e9
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2546047
Tested-by: Amin Hassani <ahassani@chromium.org>
Commit-Queue: Amin Hassani <ahassani@chromium.org>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
diff --git a/Makefile b/Makefile
index fe2f97c..e584dcc 100644
--- a/Makefile
+++ b/Makefile
@@ -14,6 +14,7 @@
 	install -m 0755 host/start_devserver "${DESTDIR}/usr/bin"
 	install -m 0755 devserver.py strip_package.py "${DESTDIR}/usr/lib/devserver"
 	install -m 0644  \
+		autoupdate.py \
 		builder.py \
 		cherrypy_ext.py \
 		health_checker.py \
diff --git a/autoupdate.py b/autoupdate.py
new file mode 100644
index 0000000..88def64
--- /dev/null
+++ b/autoupdate.py
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011 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.
+
+"""Devserver module for handling update client requests."""
+
+from __future__ import print_function
+
+import os
+
+from six.moves import urllib
+
+import cherrypy  # pylint: disable=import-error
+
+# TODO(crbug.com/872441): We try to import nebraska from different places
+# because when we install the devserver, we copy the nebraska.py into the main
+# directory. Once this bug is resolved, we can always import from nebraska
+# directory.
+try:
+  from nebraska import nebraska
+except ImportError:
+  import nebraska
+
+import setup_chromite  # pylint: disable=unused-import
+from chromite.lib.xbuddy import cherrypy_log_util
+from chromite.lib.xbuddy import devserver_constants as constants
+
+
+# Module-local log function.
+def _Log(message, *args):
+  return cherrypy_log_util.LogWithTag('UPDATE', message, *args)
+
+class AutoupdateError(Exception):
+  """Exception classes used by this module."""
+  pass
+
+
+def _ChangeUrlPort(url, new_port):
+  """Return the URL passed in with a different port"""
+  scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
+  host_port = netloc.split(':')
+
+  if len(host_port) == 1:
+    host_port.append(new_port)
+  else:
+    host_port[1] = new_port
+
+  print(host_port)
+  netloc = '%s:%s' % tuple(host_port)
+
+  # pylint: disable=too-many-function-args
+  return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
+
+def _NonePathJoin(*args):
+  """os.path.join that filters None's from the argument list."""
+  return os.path.join(*[x for x in args if x is not None])
+
+
+class Autoupdate(object):
+  """Class that contains functionality that handles Chrome OS update pings."""
+
+  def __init__(self, xbuddy, static_dir=None):
+    """Initializes the class.
+
+    Args:
+      xbuddy: The xbuddy path.
+      static_dir: The path to the devserver static directory.
+    """
+    self.xbuddy = xbuddy
+    self.static_dir = static_dir
+
+  def GetUpdateForLabel(self, label):
+    """Given a label, get an update from the directory.
+
+    Args:
+      label: the relative directory inside the static dir
+
+    Returns:
+      A relative path to the directory with the update payload.
+      This is the label if an update did not need to be generated, but can
+      be label/cache/hashed_dir_for_update.
+
+    Raises:
+      AutoupdateError: If client version is higher than available update found
+        at the directory given by the label.
+    """
+    _Log('Update label: %s', label)
+    static_update_path = _NonePathJoin(self.static_dir, label,
+                                       constants.UPDATE_FILE)
+
+    if label and os.path.exists(static_update_path):
+      # An update payload was found for the given label, return it.
+      return label
+
+    # The label didn't resolve.
+    _Log('Did not found any update payload for label %s.', label)
+    return None
+
+  def GetDevserverUrl(self):
+    """Returns the devserver url base."""
+    x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
+    if x_forwarded_host:
+      # Select the left most <ip>:<port> value so that the request is
+      # forwarded correctly.
+      x_forwarded_host = [x.strip() for x in x_forwarded_host.split(',')][0]
+      hostname = 'http://' + x_forwarded_host
+    else:
+      hostname = cherrypy.request.base
+
+    return hostname
+
+  def GetStaticUrl(self):
+    """Returns the static url base that should prefix all payload responses."""
+    hostname = self.GetDevserverUrl()
+    _Log('Handling update ping as %s', hostname)
+
+    static_urlbase = '%s/static' % hostname
+    _Log('Using static url base %s', static_urlbase)
+    return static_urlbase
+
+  def GetPathToPayload(self, label, board):
+    """Find a payload locally.
+
+    See devserver's update rpc for documentation.
+
+    Args:
+      label: from update request
+      board: from update request
+
+    Returns:
+      The relative path to an update from the static_dir
+
+    Raises:
+      AutoupdateError: If the update could not be found.
+    """
+    label = label or ''
+    label_list = label.split('/')
+    # Suppose that the path follows old protocol of indexing straight
+    # into static_dir with board/version label.
+    # Attempt to get the update in that directory, generating if necc.
+    path_to_payload = self.GetUpdateForLabel(label)
+    if path_to_payload is None:
+      # There was no update found in the directory. Let XBuddy find the
+      # payloads.
+      if label_list[0] == 'xbuddy':
+        # If path explicitly calls xbuddy, pop off the tag.
+        label_list.pop()
+      x_label, _ = self.xbuddy.Translate(label_list, board=board)
+      # Path has been resolved, try to get the payload.
+      path_to_payload = self.GetUpdateForLabel(x_label)
+      if path_to_payload is None:
+        # No update payload found after translation. Try to get an update to
+        # a test image from GS using the label.
+        path_to_payload, _image_name = self.xbuddy.Get(
+            ['remote', label, 'full_payload'])
+
+    # One of the above options should have gotten us a relative path.
+    if path_to_payload is None:
+      raise AutoupdateError('Failed to get an update for: %s' % label)
+
+    return path_to_payload
+
+  def HandleUpdatePing(self, data, label='', **kwargs):
+    """Handles an update ping from an update client.
+
+    Args:
+      data: XML blob from client.
+      label: optional label for the update.
+      kwargs: The map of query strings passed to the /update API.
+
+    Returns:
+      Update payload message for client.
+    """
+    # Get the static url base that will form that base of our update url e.g.
+    # http://hostname:8080/static/update.gz.
+    static_urlbase = self.GetStaticUrl()
+    # Change the URL's string query dictionary provided by cherrypy to a valid
+    # dictionary that has proper values for its keys. e.g. True instead of
+    # 'True'.
+    kwargs = nebraska.QueryDictToDict(kwargs)
+
+    # Process attributes of the update check.
+    request = nebraska.Request(data)
+    if request.request_type == nebraska.Request.RequestType.EVENT:
+      _Log('A non-update event notification received. Returning an ack.')
+      return nebraska.Nebraska().GetResponseToRequest(
+          request, response_props=nebraska.ResponseProperties(**kwargs))
+
+    _Log('Update Check Received.')
+
+    try:
+      path_to_payload = self.GetPathToPayload(label, request.board)
+      base_url = _NonePathJoin(static_urlbase, path_to_payload)
+      local_payload_dir = _NonePathJoin(self.static_dir, path_to_payload)
+    except AutoupdateError as e:
+      # Raised if we fail to generate an update payload.
+      _Log('Failed to process an update request, but we will defer to '
+           'nebraska to respond with no-update. The error was %s', e)
+
+    _Log('Responding to client to use url %s to get image', base_url)
+    nebraska_props = nebraska.NebraskaProperties(
+        update_payloads_address=base_url,
+        update_metadata_dir=local_payload_dir)
+    nebraska_obj = nebraska.Nebraska(nebraska_props=nebraska_props)
+    return nebraska_obj.GetResponseToRequest(
+        request, response_props=nebraska.ResponseProperties(**kwargs))
diff --git a/autoupdate_unittest.py b/autoupdate_unittest.py
new file mode 100755
index 0000000..648ef7c
--- /dev/null
+++ b/autoupdate_unittest.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 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.
+
+"""Unit tests for autoupdate.py."""
+
+from __future__ import print_function
+
+import json
+import shutil
+import socket
+import tempfile
+import unittest
+
+import mock
+import cherrypy  # pylint: disable=import-error
+
+import autoupdate
+
+import setup_chromite  # pylint: disable=unused-import
+from chromite.lib.xbuddy import common_util
+from chromite.lib.xbuddy import xbuddy
+
+
+_TEST_REQUEST = """<?xml version="1.0" encoding="UTF-8"?>
+<request protocol="3.0" updater="ChromeOSUpdateEngine" updaterversion="0.1.0.0">
+  <app appid="test-appid" version="%(version)s" track="%(track)s"
+       board="%(board)s" hardware_class="%(hwclass)s">
+    <updatecheck />
+  </app>
+</request>"""
+
+class AutoupdateTest(unittest.TestCase):
+  """Tests for the autoupdate.Autoupdate class."""
+
+  def setUp(self):
+    self.port = 8080
+    self.test_board = 'test-board'
+    self.build_root = tempfile.mkdtemp('autoupdate_build_root')
+    self.tempdir = tempfile.mkdtemp('tempdir')
+    self.latest_dir = '12345_af_12-a1'
+    self.latest_verision = '12345_af_12'
+    self.static_image_dir = tempfile.mkdtemp('autoupdate_static_dir')
+    self.hostname = '%s:%s' % (socket.gethostname(), self.port)
+    self.test_dict = {
+        'version': 'ForcedUpdate',
+        'track': 'test-channel',
+        'board': self.test_board,
+        'hwclass': 'test-hardware-class',
+    }
+    self.test_data = _TEST_REQUEST % self.test_dict
+    self.payload = 'My payload'
+    cherrypy.request.base = 'http://%s' % self.hostname
+    common_util.MkDirP(self.static_image_dir)
+    self._xbuddy = xbuddy.XBuddy(False,
+                                 static_dir=self.static_image_dir)
+    mock.patch.object(xbuddy.XBuddy, '_GetArtifact')
+
+  def tearDown(self):
+    shutil.rmtree(self.build_root)
+    shutil.rmtree(self.tempdir)
+    shutil.rmtree(self.static_image_dir)
+
+  def _DummyAutoupdateConstructor(self, **kwargs):
+    """Creates a dummy autoupdater.  Used to avoid using constructor."""
+    dummy = autoupdate.Autoupdate(self._xbuddy,
+                                  static_dir=self.static_image_dir,
+                                  **kwargs)
+    return dummy
+
+  def testChangeUrlPort(self):
+    # pylint: disable=protected-access
+    r = autoupdate._ChangeUrlPort('http://fuzzy:8080/static', 8085)
+    self.assertEqual(r, 'http://fuzzy:8085/static')
+
+    r = autoupdate._ChangeUrlPort('http://fuzzy/static', 8085)
+    self.assertEqual(r, 'http://fuzzy:8085/static')
+
+    r = autoupdate._ChangeUrlPort('ftp://fuzzy/static', 8085)
+    self.assertEqual(r, 'ftp://fuzzy:8085/static')
+
+    r = autoupdate._ChangeUrlPort('ftp://fuzzy', 8085)
+    self.assertEqual(r, 'ftp://fuzzy:8085')
+
+  def testHandleHostInfoPing(self):
+    au_mock = self._DummyAutoupdateConstructor()
+    self.assertRaises(AssertionError, au_mock.HandleHostInfoPing, None)
+
+    # Setup fake host_infos entry and ensure it comes back to us in one piece.
+    test_ip = '1.2.3.4'
+    au_mock.host_infos.GetInitHostInfo(test_ip).attrs = self.test_dict
+    self.assertEqual(
+        json.loads(au_mock.HandleHostInfoPing(test_ip)), self.test_dict)
+
+  @mock.patch.object(autoupdate.Autoupdate, 'GetPathToPayload')
+  def testHandleUpdatePing(self, path_to_payload_mock):
+    """Tests HandleUpdatePing"""
+    au_mock = self._DummyAutoupdateConstructor()
+    path_to_payload_mock.return_value = self.tempdir
+    request = """<?xml version="1.0" encoding="UTF-8"?>
+<request protocol="3.0">
+  <os version="Indy" platform="Chrome OS" sp="10323.52.0_x86_64"></os>
+  <app appid="platform" version="1.0.0" delta_okay="true"
+       track="stable-channel" board="eve">
+    <ping active="1" a="1" r="1"></ping>
+    <updatecheck targetversionprefix=""></updatecheck>
+  </app>
+</request>"""
+
+    self.assertIn('error-unknownApplication', au_mock.HandleUpdatePing(request))
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/devserver.py b/devserver.py
index d713780..9f29604 100755
--- a/devserver.py
+++ b/devserver.py
@@ -4,14 +4,22 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-"""Chromium OS development server that can be used for caching files.
+"""Chromium OS development server that can be used for all forms of update.
 
-The devserver is configured to stage and serve artifacts from Google Storage
-using the credentials provided to it before it is run. The easiest way to
-understand this is that the devserver is functioning as a local cache for
-artifacts produced and uploaded by build servers. Users of this form of
-devserver can download the artifacts from the devservers static directory.
-Archive mode is always active.
+This devserver can be used to perform system-wide autoupdate and update
+of specific portage packages on devices running Chromium OS derived operating
+systems.
+
+The devserver is configured to stage and
+serve artifacts from Google Storage using the credentials provided to it before
+it is run. The easiest way to understand this is that the devserver is
+functioning as a local cache for artifacts produced and uploaded by build
+servers. Users of this form of devserver can either download the artifacts
+from the devservers static directory OR use the update RPC to perform a
+system-wide autoupdate. Archive mode is always active.
+
+For autoupdates, there are many more advanced options that can help specify
+how to update and which payload to give to a requester.
 """
 
 from __future__ import print_function
@@ -37,6 +45,7 @@
 from cherrypy.process import plugins
 # pylint: enable=no-name-in-module, import-error
 
+import autoupdate
 import cherrypy_ext
 import health_checker
 
@@ -66,6 +75,9 @@
                   'dep-chrome_test.tar.bz2',
                   'dep-perf_data_dep.tar.bz2']
 
+# Sets up global to share between classes.
+updater = None
+
 # Log rotation parameters.  These settings correspond to twice a day once
 # devserver is started, with about two weeks (28 backup files) of old logs
 # kept for backup.
@@ -128,23 +140,19 @@
     raise DevServerError('Must specify an archive_url in the request')
 
 
-def _canonicalize_local_path(local_path, static_dir):
+def _canonicalize_local_path(local_path):
   """Canonicalizes |local_path| strings.
 
-  Args:
-    local_path: The input path.
-    static_dir: Devserver's static cache directory.
-
   Raises:
     DevserverError: if |local_path| is not set.
   """
   # Restrict staging of local content to only files within the static
   # directory.
   local_path = os.path.abspath(local_path)
-  if not local_path.startswith(static_dir):
+  if not local_path.startswith(updater.static_dir):
     raise DevServerError(
         'Local path %s must be a subdirectory of the static'
-        ' directory: %s' % (local_path, static_dir))
+        ' directory: %s' % (local_path, updater.static_dir))
 
   return local_path.rstrip('/')
 
@@ -183,21 +191,20 @@
   return os_type == 'android'
 
 
-def _get_downloader(static_dir, kwargs):
+def _get_downloader(kwargs):
   """Returns the downloader based on passed in arguments.
 
   Args:
-    static_dir: Devserver's static cache directory.
     kwargs: Keyword arguments for the request.
   """
   local_path = kwargs.get('local_path')
   if local_path:
-    local_path = _canonicalize_local_path(local_path, static_dir)
+    local_path = _canonicalize_local_path(local_path)
 
   dl = None
   if local_path:
     delete_source = _parse_boolean_arg(kwargs, 'delete_source')
-    dl = downloader.LocalDownloader(static_dir, local_path,
+    dl = downloader.LocalDownloader(updater.static_dir, local_path,
                                     delete_source=delete_source)
 
   if not _is_android_build_request(kwargs):
@@ -211,7 +218,7 @@
     if not dl:
       archive_url = _canonicalize_archive_url(archive_url)
       dl = downloader.GoogleStorageDownloader(
-          static_dir, archive_url,
+          updater.static_dir, archive_url,
           downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
               archive_url))
   elif not dl:
@@ -221,21 +228,20 @@
     if not target or not branch or not build_id:
       raise DevServerError('target, branch, build ID must all be specified for '
                            'downloading Android build.')
-    dl = downloader.AndroidBuildDownloader(static_dir, branch, build_id,
+    dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
                                            target)
 
   return dl
 
 
-def _get_downloader_and_factory(static_dir, kwargs):
+def _get_downloader_and_factory(kwargs):
   """Returns the downloader and artifact factory based on passed in arguments.
 
   Args:
-    static_dir: Devserver's static cache directory.
     kwargs: Keyword arguments for the request.
   """
   artifacts, files = _get_artifacts(kwargs)
-  dl = _get_downloader(static_dir, kwargs)
+  dl = _get_downloader(kwargs)
 
   if (isinstance(dl, (downloader.GoogleStorageDownloader,
                       downloader.LocalDownloader))):
@@ -339,6 +345,11 @@
       '/build': {
           'response.timeout': 100000,
       },
+      '/update': {
+          # Gets rid of cherrypy parsing post file for args.
+          'request.process_request_body': False,
+          'response.timeout': 10000,
+      },
       # Sets up the static dir for file hosting.
       '/static': {
           'tools.staticdir.dir': options.static_dir,
@@ -509,11 +520,10 @@
   # Lock used to lock increasing/decreasing count.
   _staging_thread_count_lock = threading.Lock()
 
-  def __init__(self, _xbuddy, static_dir):
+  def __init__(self, _xbuddy):
     self._builder = None
     self._telemetry_lock_dict = common_util.LockDict()
     self._xbuddy = _xbuddy
-    self._static_dir = static_dir
 
   @property
   def staging_thread_count(self):
@@ -552,7 +562,7 @@
     Returns:
       True of all artifacts are staged.
     """
-    dl, factory = _get_downloader_and_factory(self._static_dir, kwargs)
+    dl, factory = _get_downloader_and_factory(kwargs)
     response = str(dl.IsStaged(factory))
     _Log('Responding to is_staged %s request with %r', kwargs, response)
     return response
@@ -572,7 +582,7 @@
     Returns:
       A string with information about the contents of the image directory.
     """
-    dl = _get_downloader(self._static_dir, kwargs)
+    dl = _get_downloader(kwargs)
     try:
       image_dir_contents = dl.ListBuildDir()
     except build_artifact.ArtifactDownloadError as e:
@@ -634,7 +644,7 @@
         custom post-processing.
       clean: True to remove any previously staged artifacts first.
     """
-    dl, factory = _get_downloader_and_factory(self._static_dir, kwargs)
+    dl, factory = _get_downloader_and_factory(kwargs)
 
     with DevServerRoot._staging_thread_count_lock:
       DevServerRoot._staging_thread_count += 1
@@ -670,7 +680,7 @@
     if is_deprecated_server():
       raise DeprecatedRPCError('locate_file')
 
-    dl, _ = _get_downloader_and_factory(self._static_dir, kwargs)
+    dl, _ = _get_downloader_and_factory(kwargs)
     try:
       file_name = kwargs['file_name']
       artifacts = kwargs['artifacts']
@@ -704,7 +714,7 @@
     Returns:
       Path to the source folder for the telemetry codebase once it is staged.
     """
-    dl = _get_downloader(self._static_dir, kwargs)
+    dl = _get_downloader(kwargs)
 
     build_path = dl.GetBuildDir()
     deps_path = os.path.join(build_path, 'autotest/packages')
@@ -764,7 +774,7 @@
     # Try debug.tar.xz first, then debug.tgz
     for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
       kwargs['artifacts'] = artifact
-      dl = _get_downloader(self._static_dir, kwargs)
+      dl = _get_downloader(kwargs)
 
       try:
         if self.stage(**kwargs) == 'Success':
@@ -835,7 +845,7 @@
 
     try:
       return common_util.GetLatestBuildVersion(
-          self._static_dir, kwargs['target'],
+          updater.static_dir, kwargs['target'],
           milestone=kwargs.get('milestone'))
     except common_util.CommonUtilError as errmsg:
       raise DevServerHTTPError(http_client.INTERNAL_SERVER_ERROR,
@@ -873,13 +883,13 @@
 
     control_file_list = [
         line.rstrip() for line in common_util.GetControlFileListForSuite(
-            self._static_dir, kwargs['build'],
+            updater.static_dir, kwargs['build'],
             kwargs['suite_name']).splitlines()]
 
     control_file_content_dict = {}
     for control_path in control_file_list:
       control_file_content_dict[control_path] = (common_util.GetControlFile(
-          self._static_dir, kwargs['build'], control_path))
+          updater.static_dir, kwargs['build'], control_path))
 
     return json.dumps(control_file_content_dict)
 
@@ -922,13 +932,13 @@
     if 'control_path' not in kwargs:
       if 'suite_name' in kwargs and kwargs['suite_name']:
         return common_util.GetControlFileListForSuite(
-            self._static_dir, kwargs['build'], kwargs['suite_name'])
+            updater.static_dir, kwargs['build'], kwargs['suite_name'])
       else:
         return common_util.GetControlFileList(
-            self._static_dir, kwargs['build'])
+            updater.static_dir, kwargs['build'])
     else:
       return common_util.GetControlFile(
-          self._static_dir, kwargs['build'], kwargs['control_path'])
+          updater.static_dir, kwargs['build'], kwargs['control_path'])
 
   @cherrypy.expose
   def xbuddy_translate(self, *args, **kwargs):
@@ -1063,7 +1073,7 @@
     """Shows the documentation for available methods / URLs.
 
     Examples:
-      http://myhost/doc/xbuddy
+      http://myhost/doc/update
     """
     if is_deprecated_server():
       raise DeprecatedRPCError('doc')
@@ -1076,6 +1086,51 @@
       raise DevServerError("No documentation for exposed method `%s'" % name)
     return '<pre>\n%s</pre>' % method.__doc__
 
+  @cherrypy.expose
+  def update(self, *args, **kwargs):
+    """Handles an update check from a Chrome OS client.
+
+    The HTTP request should contain the standard Omaha-style XML blob. The URL
+    line may contain an additional intermediate path to the update payload.
+
+    This request can be handled in one of 4 ways, depending on the devsever
+    settings and intermediate path.
+
+    1. No intermediate path. DEPRECATED
+
+    2. Path explicitly invokes XBuddy
+    If there is a path given, it can explicitly invoke xbuddy by prefixing it
+    with 'xbuddy'. This path is then used to acquire an image binary for the
+    devserver to generate an update payload from. Devserver then serves this
+    payload.
+
+    3. Path is left for the devserver to interpret.
+    If the path given doesn't explicitly invoke xbuddy, devserver will attempt
+    to generate a payload from the test image in that directory and serve it.
+
+    Examples:
+      2. Explicitly invoke xbuddy
+      update_engine_client --omaha_url=
+      http://myhost/update/xbuddy/remote/board/version/dev
+      This would go to GS to download the dev image for the board, from which
+      the devserver would generate a payload to serve.
+
+      3. Give a path for devserver to interpret
+      update_engine_client --omaha_url=http://myhost/update/some/random/path
+      This would attempt, in order to:
+        a) Generate an update from a test image binary if found in
+           static_dir/some/random/path.
+        b) Serve an update payload found in static_dir/some/random/path.
+        c) Hope that some/random/path takes the form "board/version" and
+           and attempt to download an update payload for that board/version
+           from GS.
+    """
+    label = '/'.join(args)
+    body_length = int(cherrypy.request.headers.get('Content-Length', 0))
+    data = cherrypy.request.rfile.read(body_length)
+
+    return updater.HandleUpdatePing(data, label, **kwargs)
+
 
 def _CleanCache(cache_dir, wipe):
   """Wipes any excess cached items in the cache_dir.
@@ -1223,10 +1278,15 @@
   if options.clear_cache and options.xbuddy_manage_builds:
     _xbuddy.CleanCache()
 
+  # We allow global use here to share with cherrypy classes.
+  # pylint: disable=W0603
+  global updater
+  updater = autoupdate.Autoupdate(_xbuddy, static_dir=options.static_dir)
+
   if options.exit:
     return
 
-  dev_server = DevServerRoot(_xbuddy, options.static_dir)
+  dev_server = DevServerRoot(_xbuddy)
   health_checker_app = health_checker.Root(dev_server, options.static_dir)
 
   if options.pidfile:
diff --git a/devserver_integration_test.py b/devserver_integration_test.py
index 89c5d19..64a492f 100755
--- a/devserver_integration_test.py
+++ b/devserver_integration_test.py
@@ -26,8 +26,12 @@
 
 from string import Template
 
+from xml.dom import minidom
+
 import requests
 
+from six.moves import urllib
+
 import psutil  # pylint: disable=import-error
 
 import setup_chromite  # pylint: disable=unused-import
@@ -211,6 +215,64 @@
     self.pid = None
     self.devserver = None
 
+
+  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
+    can interpret the payload and give us back the right payload.
+
+    Args:
+      label: Label that update is served from e.g. <board>-release/<version>
+      use_test_payload: If set to true, expects to serve payload under
+        testdata/ and does extra checks i.e. compares hash and content of
+        payload.
+      appid: The APP ID of the board.
+
+    Returns:
+      url of the update payload if we verified the update.
+    """
+    update_label = '/'.join([UPDATE, label])
+    response = self._MakeRPC(
+        update_label, data=UPDATE_REQUEST.substitute({'appid': appid}),
+        critical_update=True)
+    self.assertNotEqual('', response)
+    self.assertIn('deadline="now"', 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])
+    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:
+      connection = urllib.request.urlopen(url)
+      contents = connection.read().decode('utf-8')
+      connection.close()
+      self.assertEqual('Developers, developers, developers!\n', contents)
+
+    return url
+
+  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]
+    url = urls.getElementsByTagName('url')[0]
+
+    static_url = url.getAttribute('codebase')
+    # Static url's end in /.
+    self.assertEqual(expected_static_url + '/', static_url)
+
+    manifest = update.getElementsByTagName('manifest')[0]
+    packages = manifest.getElementsByTagName('packages')[0]
+    package = packages.getElementsByTagName('package')[0]
+    filename = package.getAttribute('name')
+    self.assertEqual(TEST_UPDATE_PAYLOAD_NAME, filename)
+
+    return os.path.join(static_url, filename)
+
   def _MakeRPC(self, rpc, data=None, timeout=None, **kwargs):
     """Makes an RPC call to the devserver.
 
@@ -276,6 +338,9 @@
   they are lumped with the remote tests here.
   """
 
+  def testHandleUpdateV3(self):
+    self.VerifyHandleUpdate(label=LABEL)
+
   def testXBuddyLocalAlias(self):
     """Extensive local image xbuddy unittest.
 
@@ -310,7 +375,7 @@
 
       xbuddy_path = '/'.join([build_id, item])
       logging.info('Testing xbuddy path %s', xbuddy_path)
-      response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), timeout=3)
+      response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]))
       self.assertEqual(response, data)
 
       expected_dir = '/'.join([self.devserver_url, STATIC, build_id])
@@ -321,6 +386,14 @@
                                relative_path=True)
       self.assertEqual(response, build_id)
 
+    logging.info('Verifying the actual payload data')
+    url = self.VerifyHandleUpdate(build_id, use_test_payload=False)
+    logging.info('Verify the actual content of the update payload')
+    connection = urllib.request.urlopen(url)
+    contents = connection.read().decode('utf-8')
+    connection.close()
+    self.assertEqual(update_data, contents)
+
   def testPidFile(self):
     """Test that using a pidfile works correctly."""
     with open(self.pidfile, 'r') as f:
@@ -339,6 +412,37 @@
   2) time. These tests actually download the artifacts needed.
   """
 
+  def testStageAndUpdate(self):
+    """Tests core stage/update autotest workflow where with a test payload."""
+    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,
+                             artifacts='full_payload,stateful')
+    self.assertEqual(response, 'False')
+
+    logging.info('Staging update artifacts')
+    self._MakeRPC(STAGE, archive_url=archive_url,
+                  artifacts='full_payload,stateful')
+    logging.info('Staging complete. '
+                 'Verifying files exist and are staged in the staging '
+                 'directory.')
+    response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
+                             artifacts='full_payload,stateful')
+    self.assertEqual(response, 'True')
+    staged_dir = os.path.join(self.test_data_path, build_id)
+    self.assertTrue(os.path.isdir(staged_dir))
+    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,
+                            appid='{01906EA2-3EB2-41F1-8F62-F0B7120EFD2E}')
+
   @unittest.skip('crbug.com/640063 Broken test.')
   def testStageAutotestAndGetPackages(self):
     """Another stage/update autotest workflow test with a test payload."""