dev: add initial APIs to devserver to support cros auto-update.

This CL adds three APIs on devserver:
1. 'cros_au' API: to support 'start cros auto-update'.
2. 'get_au_status' API: to check the status of cros auto-update.
3. 'collect_au_log' API: to collect auto-update log from devserver.
4. 'handler_cleanup' API: delete the status file for tracking the progress of
   CrOS auto-update.
5. 'kill_au_proc' API: to kill unexpected auto-update process for DUT.

Also it updates the unittests and integration test.

The 'cros_au' API triggers a background process to support the whole
auto-update processes for a CrOS host, includes:
1. Transfer devserver/stateful update package.
2. If devserver cannot run on the host, restore the stateful partition.
3. If restore_stateful_partiton is not required, and stateful_update is
required, do stateful_update.
4. If stateful_update fails, or rootfs_update is required, do rootfs update.
5. Final check after the whole auto-update process.

BUG=chromium:613765
TEST=Locally ran 'ds.auto_update([dut], [image_path])';
Ran 'repair' for dut from local autotest instance;
Ran unittest, devserver_integration_test.

 Changes to be committed:
	modified:   Makefile
	new file:   cros_update.py
	new file:   cros_update_logging.py
	new file:   cros_update_progress.py
	new file:   cros_update_unittest.py
	modified:   devserver.py
	modified:   devserver_integration_test.py

Change-Id: I2e9c116bc1e0b07d37b540266fd252aee4fd6e84
Reviewed-on: https://chromium-review.googlesource.com/346199
Commit-Ready: Xixuan Wu <xixuan@chromium.org>
Tested-by: Xixuan Wu <xixuan@chromium.org>
Reviewed-by: Xixuan Wu <xixuan@chromium.org>
diff --git a/Makefile b/Makefile
index f057718..ee1e9f2 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,9 @@
 		builder.py \
 		cherrypy_ext.py \
 		common_util.py \
+		cros_update.py \
+		cros_update_logging.py \
+		cros_update_progress.py \
 		devserver_constants.py \
 		downloader.py \
 		gsutil_util.py \
diff --git a/cros_update.py b/cros_update.py
new file mode 100644
index 0000000..3239ce4
--- /dev/null
+++ b/cros_update.py
@@ -0,0 +1,275 @@
+# Copyright (c) 2016 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.
+
+"""An executable function cros-update for auto-update of a CrOS host.
+
+The reason to create this file is to let devserver to trigger a background
+process for CrOS auto-update. Therefore, when devserver service is restarted
+sometimes, the CrOS auto-update process is still running and the corresponding
+provision task won't claim failure.
+
+It includes two classes:
+  a. CrOSUpdateTrigger:
+    1. Includes all logics which identify which types of update need to be
+       performed in the current DUT.
+    2. Responsible for write current status of CrOS auto-update process into
+       progress_tracker.
+
+  b. CrOSAUParser:
+    1. Pre-setup the required args for CrOS auto-update.
+    2. Parse the input parameters for cmd that runs 'cros_update.py'.
+"""
+
+from __future__ import print_function
+
+import argparse
+import cros_update_logging
+import cros_update_progress
+import logging
+import os
+import sys
+
+try:
+  from chromite.lib import auto_updater
+  from chromite.lib import remote_access
+  from chromite.lib import timeout_util
+except ImportError as e:
+  logging.debug('chromite cannot be imported: %r', e)
+  auto_updater = None
+  remote_access = None
+
+# Timeout for CrOS auto-update process.
+CROS_UPDATE_TIMEOUT_MIN = 30
+
+# The preserved path in remote device, won't be deleted after rebooting.
+CROS_PRESERVED_PATH = ('/mnt/stateful_partition/unencrypted/'
+                       'preserve/cros-update')
+
+# Standard error tmeplate to be written into status tracking log.
+CROS_ERROR_TEMPLATE = cros_update_progress.ERROR_TAG + ' %r'
+
+
+class CrOSAUParser(object):
+  """Custom command-line options parser for cros-update."""
+  def __init__(self):
+    self.args = sys.argv[1:]
+    self.parser = argparse.ArgumentParser(
+        usage='%(prog)s [options] [control-file]')
+    self.SetupOptions()
+    self.removed_args = []
+
+    # parse an empty list of arguments in order to set self.options
+    # to default values.
+    self.options = self.parser.parse_args(args=[])
+
+  def SetupOptions(self):
+    """Setup options to call cros-update command."""
+    self.parser.add_argument('-d', action='store', type=str,
+                             dest='host_name',
+                             help='host_name of a DUT')
+    self.parser.add_argument('-b', action='store', type=str,
+                             dest='build_name',
+                             help='build name to be auto-updated')
+    self.parser.add_argument('--static_dir', action='store', type=str,
+                             dest='static_dir',
+                             help='static directory of the devserver')
+    self.parser.add_argument('--force_update', action='store_true',
+                             dest='force_update', default=False,
+                             help=('force an update even if the version '
+                                   'installed is the same'))
+    self.parser.add_argument('--full_update', action='store_true',
+                             dest='full_update', default=False,
+                             help=('force a rootfs update, skip stateful '
+                                   'update'))
+
+  def ParseArgs(self):
+    """Parse and process command line arguments."""
+    # Positional arguments from the end of the command line will be included
+    # in the list of unknown_args.
+    self.options, unknown_args = self.parser.parse_known_args()
+    # Filter out none-positional arguments
+    while unknown_args and unknown_args[0][0] == '-':
+      self.removed_args.append(unknown_args.pop(0))
+      # Always assume the argument has a value.
+      if unknown_args:
+        self.removed_args.append(unknown_args.pop(0))
+    if self.removed_args:
+      logging.warn('Unknown arguments are removed from the options: %s',
+                   self.removed_args)
+
+
+class CrOSUpdateTrigger(object):
+  """The class for CrOS auto-updater trigger.
+
+  This class is used for running all CrOS auto-update trigger logic.
+  """
+  def __init__(self, host_name, build_name, static_dir, progress_tracker=None,
+               log_file=None, force_update=False, full_update=False):
+    self.host_name = host_name
+    self.build_name = build_name
+    self.static_dir = static_dir
+    self.progress_tracker = progress_tracker
+    self.log_file = log_file
+    self.force_update = force_update
+    self.full_update = full_update
+
+  def _WriteAUStatus(self, content):
+    if self.progress_tracker:
+      self.progress_tracker.WriteStatus(content)
+
+  def _StatefulUpdate(self, cros_updater):
+    """The detailed process in stateful update.
+
+    Args:
+      cros_updater: The CrOS auto updater for auto-update.
+    """
+    self._WriteAUStatus('pre-setup stateful update')
+    cros_updater.PreSetupStatefulUpdate()
+    self._WriteAUStatus('perform stateful update')
+    cros_updater.UpdateStateful()
+    self._WriteAUStatus('post-check stateful update')
+    cros_updater.PostCheckStatefulUpdate()
+
+  def _RootfsUpdate(self, cros_updater):
+    """The detailed process in rootfs update.
+
+    Args:
+      cros_updater: The CrOS auto updater for auto-update.
+    """
+    self._WriteAUStatus('transfer rootfs update package')
+    cros_updater.TransferRootfsUpdate()
+    self._WriteAUStatus('pre-setup rootfs update')
+    cros_updater.PreSetupRootfsUpdate()
+    self._WriteAUStatus('rootfs update')
+    cros_updater.UpdateRootfs()
+    self._WriteAUStatus('post-check rootfs update')
+    cros_updater.PostCheckRootfsUpdate()
+
+  def TriggerAU(self):
+    """Execute auto update for cros_host.
+
+    The auto update includes 4 steps:
+    1. if devserver cannot run, restore the stateful partition.
+    2. if possible, do stateful update first, but never raise errors.
+    3. If required or stateful_update fails, first do rootfs update, then do
+       stateful_update.
+    4. Post-check for the whole update.
+    """
+    try:
+      with remote_access.ChromiumOSDeviceHandler(
+          self.host_name, port=None,
+          base_dir=CROS_PRESERVED_PATH,
+          ping=True) as device:
+
+        logging.debug('Remote device %s is connected', self.host_name)
+        payload_dir = os.path.join(self.static_dir, self.build_name)
+        chromeos_AU = auto_updater.ChromiumOSUpdater(
+            device, self.build_name, payload_dir, log_file=self.log_file,
+            yes=True)
+        chromeos_AU.CheckPayloads()
+
+        self._WriteAUStatus('Transfer Devserver/Stateful Update Package')
+        chromeos_AU.TransferDevServerPackage()
+        chromeos_AU.TransferStatefulUpdate()
+
+        restore_stateful = chromeos_AU.CheckRestoreStateful()
+        do_stateful_update = (not self.full_update) and (
+            chromeos_AU.PreSetupCrOSUpdate() and self.force_update)
+        stateful_update_complete = False
+        logging.debug('Start CrOS update process...')
+        try:
+          if restore_stateful:
+            self._WriteAUStatus('Restore Stateful Partition')
+            chromeos_AU.RestoreStateful()
+            stateful_update_complete = True
+          else:
+            # Whether to execute stateful update depends on:
+            # a. full_update=False: No full reimage is required.
+            # b. The update version is matched to the current version, And
+            #    force_update=True: Update is forced even if the version
+            #    installed is the same.
+            if do_stateful_update:
+              self._StatefulUpdate(chromeos_AU)
+              stateful_update_complete = True
+
+        except Exception as e:
+          logging.debug('Error happens in stateful update: %r', e)
+
+        # Whether to execute rootfs update depends on:
+        # a. stateful update is not completed, or completed by
+        #    update action 'restore_stateful'.
+        # b. force_update=True: Update is forced no matter what the current
+        #    version is. Or, the update version is not matched to the current
+        #    version.
+        require_rootfs_update = self.force_update or (
+            not chromeos_AU.CheckVersion())
+        if (not (do_stateful_update and stateful_update_complete)
+            and require_rootfs_update):
+          self._RootfsUpdate(chromeos_AU)
+          self._StatefulUpdate(chromeos_AU)
+
+        self._WriteAUStatus('post-check for CrOS auto-update')
+        chromeos_AU.PostCheckCrOSUpdate()
+        self._WriteAUStatus(cros_update_progress.FINISHED)
+    except Exception as e:
+      logging.debug('Error happens in CrOS auto-update: %r', e)
+      self._WriteAUStatus(CROS_ERROR_TEMPLATE % e)
+      raise
+
+
+def main():
+  # Setting logging level
+  logConfig = cros_update_logging.loggingConfig()
+  logConfig.ConfigureLogging()
+
+  # Create one cros_update_parser instance for parsing CrOS auto-update cmd.
+  AU_parser = CrOSAUParser()
+  try:
+    AU_parser.ParseArgs()
+  except Exception as e:
+    logging.error('Error in Parsing Args: %r', e)
+    raise
+
+  if len(sys.argv) == 1:
+    AU_parser.parser.print_help()
+    sys.exit(1)
+
+  host_name = AU_parser.options.host_name
+  build_name = AU_parser.options.build_name
+  static_dir = AU_parser.options.static_dir
+  force_update = AU_parser.options.force_update
+  full_update = AU_parser.options.full_update
+
+  # Reset process group id to make current process running on the background.
+  pid = os.getpid()
+  os.setsid()
+
+  # Setting log files for CrOS auto-update process.
+  # Log file:  file to record every details of CrOS auto-update process.
+  log_file = cros_update_progress.GetExecuteLogFile(host_name, pid)
+  logging.info('Writing executing logs into file: %s', log_file)
+  logConfig.SetFileHandler(log_file)
+
+  # Create a progress_tracker for tracking CrOS auto-update progress.
+  progress_tracker = cros_update_progress.AUProgress(host_name, pid)
+
+  # Create cros_update instance to run CrOS auto-update.
+  cros_updater_trigger = CrOSUpdateTrigger(host_name, build_name, static_dir,
+                                           progress_tracker=progress_tracker,
+                                           log_file=log_file,
+                                           force_update=force_update,
+                                           full_update=full_update)
+
+  # Set timeout the cros-update process.
+  try:
+    with timeout_util.Timeout(CROS_UPDATE_TIMEOUT_MIN*60):
+      cros_updater_trigger.TriggerAU()
+  except timeout_util.TimeoutError as e:
+    error_msg = ('%s. The CrOS auto-update process is timed out, thus will be '
+                 'terminated' % str(e))
+    progress_tracker.WriteStatus(CROS_ERROR_TEMPLATE % error_msg)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/cros_update_logging.py b/cros_update_logging.py
new file mode 100644
index 0000000..b1fca8c
--- /dev/null
+++ b/cros_update_logging.py
@@ -0,0 +1,66 @@
+# Copyright 2016 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.
+
+"""A logging strategy for cros-update.
+
+1. Logging format is set as the same as autoserv.
+2. Globally, set log level of output as 'DEBUG'.
+3. For control output, set LEVEL as 'INFO', to avoid unnecessary logs in
+   cherrypy logs.
+4. Add file handler to record all logs above level 'DEBUG' into file.
+"""
+
+from __future__ import print_function
+
+import logging
+import sys
+
+
+class loggingConfig(object):
+  """Configuration for auto-update logging."""
+
+  LOGGING_FORMAT = ('%(asctime)s.%(msecs)03d %(levelname)-5.5s|%(module)18.18s:'
+                    '%(lineno)4.4d| %(message)s')
+
+  def __init__(self):
+    self.logger = logging.getLogger()
+    self.GLOBAL_LEVEL = logging.DEBUG
+    self.CONSOLE_LEVEL = logging.INFO
+    self.FILE_LEVEL = logging.DEBUG
+    self.ENABLE_CONSOLE_LOGGING = False
+
+  def SetControlHandler(self, stream):
+    """Set console handler for logging.
+
+    Args:
+      stream: The input stream, could be stdout/stderr.
+    """
+    handler = logging.StreamHandler(stream)
+    handler.setLevel(self.CONSOLE_LEVEL)
+    file_formatter = logging.Formatter(fmt=self.LOGGING_FORMAT,
+                                       datefmt='%Y/%m/%d %H:%M:%S')
+    handler.setFormatter(file_formatter)
+    self.logger.addHandler(handler)
+    return handler
+
+
+  def SetFileHandler(self, file_name):
+    """Set file handler for logging.
+
+    Args:
+      file_name: The file to save logs into.
+    """
+    handler = logging.FileHandler(file_name)
+    handler.setLevel(self.FILE_LEVEL)
+    # file format is set as same as concole format
+    file_formatter = logging.Formatter(fmt=self.LOGGING_FORMAT,
+                                       datefmt='%Y/%m/%d %H:%M:%S')
+    handler.setFormatter(file_formatter)
+    self.logger.addHandler(handler)
+
+
+  def ConfigureLogging(self):
+    self.logger.setLevel(self.GLOBAL_LEVEL)
+    if self.ENABLE_CONSOLE_LOGGING:
+      self.SetControlHandler(sys.stdout)
diff --git a/cros_update_progress.py b/cros_update_progress.py
new file mode 100644
index 0000000..9ab43e3
--- /dev/null
+++ b/cros_update_progress.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2016 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.
+
+"""A progress class for tracking CrOS auto-update process.
+
+This class is mainly designed for:
+  1. Set the pattern for generating the filenames of
+     track_status_file/execute_log_file.
+     track_status_file: Used for record the current step of CrOS auto-update
+                        process. Only has one line.
+     execute_log_file: Used for record the whole logging info of the CrOS
+                       auto-update process, including any debug information.
+  2. Write current auto-update process into the track_status_file.
+  3. Read current auto-update process from the track_status_file.
+
+This file also offers external functions that are related to add/check/delete
+the progress of the CrOS auto-update process.
+"""
+
+from __future__ import print_function
+
+import logging
+import os
+
+try:
+  from chromite.lib import osutils
+except ImportError as e:
+  logging.debug('chromite cannot be imported: %r', e)
+  osutils = None
+
+# Path for status tracking log.
+TRACK_LOG_FILE_PATH = '/tmp/auto-update/tracking_log/%s_%s.log'
+
+# Path for executing log.
+EXECUTE_LOG_FILE_PATH = '/tmp/auto-update/executing_log/%s_%s.log'
+
+# The string for update process finished
+FINISHED = 'Completed'
+ERROR_TAG = 'Error'
+
+
+def ReadOneLine(filename):
+  """Read one line from file.
+
+  Args:
+    filename: The file to be read.
+  """
+  return open(filename, 'r').readline().rstrip('\n')
+
+
+def IsProcessAlive(pid):
+  """Detect whether a process is alive or not.
+
+  Args:
+    pid: The process id.
+  """
+  path = '/proc/%s/stat' % pid
+  try:
+    stat = ReadOneLine(path)
+  except IOError:
+    if not os.path.exists(path):
+      return False
+
+    raise
+
+  return stat.split()[2] != 'Z'
+
+
+def GetExecuteLogFile(host_name, pid):
+  """Return the whole path of execute log file."""
+  if not os.path.exists(os.path.dirname(EXECUTE_LOG_FILE_PATH)):
+    osutils.SafeMakedirs(os.path.dirname(EXECUTE_LOG_FILE_PATH))
+
+  return EXECUTE_LOG_FILE_PATH % (host_name, pid)
+
+
+def GetTrackStatusFile(host_name, pid):
+  """Return the whole path of track status file."""
+  if not os.path.exists(os.path.dirname(TRACK_LOG_FILE_PATH)):
+    osutils.SafeMakedirs(os.path.dirname(TRACK_LOG_FILE_PATH))
+
+  return TRACK_LOG_FILE_PATH % (host_name, pid)
+
+
+def DelTrackStatusFile(host_name, pid):
+  """Delete the track status log."""
+  osutils.SafeUnlink(GetTrackStatusFile(host_name, pid))
+
+
+class AUProgress(object):
+  """Used for tracking the CrOS auto-update progress."""
+
+  def __init__(self, host_name, pid):
+    """Initialize a CrOS update progress instance.
+
+    Args:
+      host_name: The name of host, should be in the file_name of the status
+        tracking file of auto-update process.
+      pid: The process id, should be in the file_name too.
+    """
+    self.host_name = host_name
+    self.pid = pid
+
+  @property
+  def track_status_file(self):
+    """The track status file to record the CrOS auto-update progress."""
+    return GetTrackStatusFile(self.host_name, self.pid)
+
+  def WriteStatus(self, content):
+    """Write auto-update progress into status tracking file.
+
+    Args:
+      content: The content to be recorded.
+    """
+    if not self.track_status_file:
+      return
+
+    try:
+      with open(self.track_status_file, 'w') as out_log:
+        out_log.write(content)
+    except Exception as e:
+      logging.error('Cannot write au status: %r', e)
+
+  def ReadStatus(self):
+    """Read auto-update progress from status tracking file."""
+    return ReadOneLine(self.track_status_file)
diff --git a/cros_update_unittest.py b/cros_update_unittest.py
new file mode 100755
index 0000000..9aae815
--- /dev/null
+++ b/cros_update_unittest.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python2
+
+# Copyright (c) 2016 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 cros_update_parser.py."""
+
+from __future__ import print_function
+
+import sys
+import unittest
+
+import cros_update
+
+class CrosUpdateParserTest(unittest.TestCase):
+  """Tests for the autoupdate.Autoupdate class."""
+
+  def setUp(self):
+    self.orig_sys_argv = sys.argv
+
+  def tearDown(self):
+    self.argv = self.orig_sys_argv
+
+  def _get_parser(self):
+    return cros_update.CrOSAUParser()
+
+  def test_parse_args(self):
+    host_name = '100.0.0.1'
+    build_name = 'fake/image'
+    sys.argv = ['run.py', '-d', host_name, '-b', build_name, '-q', 'test']
+    parser = self._get_parser()
+    parser.ParseArgs()
+    self.assertEqual(host_name, parser.options.host_name)
+    self.assertEqual(build_name, parser.options.build_name)
+    self.assertEqual(['-q', 'test'], parser.removed_args)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/devserver.py b/devserver.py
index 936966e..71351ac 100755
--- a/devserver.py
+++ b/devserver.py
@@ -41,11 +41,13 @@
 
 from __future__ import print_function
 
+import glob
 import json
 import optparse
 import os
 import re
 import shutil
+import signal
 import socket
 import subprocess
 import sys
@@ -63,6 +65,8 @@
 import artifact_info
 import build_artifact
 import cherrypy_ext
+import cros_update
+import cros_update_progress
 import common_util
 import devserver_constants
 import downloader
@@ -125,6 +129,15 @@
 # Number of seconds between the collection of disk and network IO counters.
 STATS_INTERVAL = 10.0
 
+# Auto-update parameters
+
+# Error msg for missing key in CrOS auto-update.
+KEY_ERROR_MSG = 'Key Error in cmd %s: %s= is required'
+
+# Command of running auto-update.
+AUTO_UPDATE_CMD = '/usr/bin/python -u %s -d %s -b %s --static_dir %s'
+
+
 class DevServerError(Exception):
   """Exception class used by this module."""
 
@@ -465,6 +478,27 @@
   return method_list
 
 
+def _check_base_args_for_auto_update(kwargs):
+  if 'host_name' not in kwargs:
+    raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'host_name')
+
+  if 'build_name' not in kwargs:
+    raise common_util.DevServerHTTPError(KEY_ERROR_MSG % 'build_name')
+
+
+def _parse_boolean_arg(kwargs, key):
+  if key in kwargs:
+    if kwargs[key] == 'True':
+      return True
+    elif kwargs[key] == 'False':
+      return False
+    else:
+      raise common_util.DevServerHTTPError(
+          'The value for key %s is not boolean.' % key)
+  else:
+    return False
+
+
 class ApiRoot(object):
   """RESTful API for Dev Server information."""
   exposed = True
@@ -762,6 +796,187 @@
     return 'Success'
 
   @cherrypy.expose
+  def cros_au(self, **kwargs):
+    """Auto-update a CrOS DUT.
+
+    Args:
+      kwargs:
+        host_name: the hostname of the DUT to auto-update.
+        build_name: the build name for update the DUT.
+        force_update: Force an update even if the version installed is the
+          same. Default: False.
+        full_update: If True, do not run stateful update, directly force a full
+          reimage. If False, try stateful update first if the dut is already
+          installed with the same version.
+        async: Whether the auto_update function is ran in the background.
+
+    Returns:
+      A tuple includes two elements:
+          a boolean variable represents whether the auto-update process is
+              successfully started.
+          an integer represents the background auto-update process id.
+    """
+    _check_base_args_for_auto_update(kwargs)
+
+    host_name = kwargs['host_name']
+    build_name = kwargs['build_name']
+    force_update = _parse_boolean_arg(kwargs, 'force_update')
+    full_update = _parse_boolean_arg(kwargs, 'full_update')
+    async = _parse_boolean_arg(kwargs, 'async')
+
+    if async:
+      path = os.path.dirname(os.path.abspath(__file__))
+      execute_file = os.path.join(path, 'cros_update.py')
+      args = (AUTO_UPDATE_CMD % (execute_file, host_name, build_name,
+                                 updater.static_dir))
+      if force_update:
+        args = ('%s --force_update' % args)
+
+      if full_update:
+        args = ('%s --full_update' % args)
+
+      p = subprocess.Popen([args], shell=True)
+
+      # Pre-write status in the track_status_file before the first call of
+      # 'get_au_status' to make sure that the track_status_file exists.
+      progress_tracker = cros_update_progress.AUProgress(host_name, p.pid)
+      progress_tracker.WriteStatus('CrOS update is just started.')
+
+      return json.dumps((True, p.pid))
+    else:
+      cros_update_trigger = cros_update.CrOSUpdateTrigger(
+          host_name, build_name, updater.static_dir)
+      cros_update_trigger.TriggerAU()
+
+  @cherrypy.expose
+  def get_au_status(self, **kwargs):
+    """Check if the auto-update task is finished.
+
+    It handles 4 cases:
+    1. If an error exists in the track_status_file, delete the track file and
+       raise it.
+    2. If cros-update process is finished, delete the file and return the
+       success result.
+    3. If the process is not running, delete the track file and raise an error
+       about 'the process is terminated due to unknown reason'.
+    4. If the track_status_file does not exist, kill the process if it exists,
+       and raise the IOError.
+
+    Args:
+      kwargs:
+        host_name: the hostname of the DUT to auto-update.
+        pid: the background process id of cros-update.
+
+    Returns:
+      A tuple includes two elements:
+          a boolean variable represents whether the auto-update process is
+              finished.
+          a string represents the current auto-update process status.
+              For example, 'Transfer Devserver/Stateful Update Package'.
+    """
+    if 'host_name' not in kwargs:
+      raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
+
+    if 'pid' not in kwargs:
+      raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
+
+    host_name = kwargs['host_name']
+    pid = kwargs['pid']
+    progress_tracker = cros_update_progress.AUProgress(host_name, pid)
+
+    try:
+      result = progress_tracker.ReadStatus()
+      if result.startswith(cros_update_progress.ERROR_TAG):
+        raise DevServerError(result[len(cros_update_progress.ERROR_TAG):])
+
+      if result == cros_update_progress.FINISHED:
+        return json.dumps((True, result))
+
+      if not cros_update_progress.IsProcessAlive(pid):
+        raise DevServerError('Cros_update process terminated midway '
+                             'due to unknown purpose. Last update status '
+                             'was %s' % result)
+
+      return json.dumps((False, result))
+    except IOError:
+      if pid:
+        os.kill(int(pid), signal.SIGKILL)
+
+      raise
+
+  @cherrypy.expose
+  def handler_cleanup(self, **kwargs):
+    """Clean track status log for CrOS auto-update process.
+
+    Args:
+      kwargs:
+        host_name: the hostname of the DUT to auto-update.
+        pid: the background process id of cros-update.
+    """
+    if 'host_name' not in kwargs:
+      raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
+
+    if 'pid' not in kwargs:
+      raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
+
+    host_name = kwargs['host_name']
+    pid = kwargs['pid']
+    cros_update_progress.DelTrackStatusFile(host_name, pid)
+
+  @cherrypy.expose
+  def kill_au_proc(self, **kwargs):
+    """Kill CrOS auto-update process using given process id.
+
+    Args:
+      kwargs:
+        host_name: Kill all the CrOS auto-update process of this host.
+
+    Returns:
+      True if all processes are killed properly.
+    """
+    if 'host_name' not in kwargs:
+      raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
+
+    host_name = kwargs['host_name']
+    file_filter = cros_update_progress.TRACK_LOG_FILE_PATH % (host_name, '*')
+    track_log_list = glob.glob(file_filter)
+    for log in track_log_list:
+      # The track log's full path is: path/host_name_pid.log
+      # Use splitext to remove file extension, then parse pid from the
+      # filename.
+      pid = os.path.splitext(os.path.basename(log))[0][len(host_name)+1:]
+      if cros_update_progress.IsProcessAlive(pid):
+        os.kill(int(pid), signal.SIGKILL)
+
+      cros_update_progress.DelTrackStatusFile(host_name, pid)
+
+    return 'True'
+
+  @cherrypy.expose
+  def collect_cros_au_log(self, **kwargs):
+    """Collect CrOS auto-update log.
+
+    Args:
+      kwargs:
+        host_name: the hostname of the DUT to auto-update.
+        pid: the background process id of cros-update.
+
+    Returns:
+      A string contains the whole content of the execute log file.
+    """
+    if 'host_name' not in kwargs:
+      raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'host_name'))
+
+    if 'pid' not in kwargs:
+      raise common_util.DevServerHTTPError((KEY_ERROR_MSG % 'pid'))
+
+    host_name = kwargs['host_name']
+    pid = kwargs['pid']
+    log_file = cros_update_progress.GetExecuteLogFile(host_name, pid)
+    with open(log_file, 'r') as f:
+      return f.read()
+
+  @cherrypy.expose
   def locate_file(self, **kwargs):
     """Get the path to the given file name.
 
@@ -777,7 +992,6 @@
     Returns:
       Path to the file with the given name. It's relative to the folder for the
       build, e.g., DATA/priv-app/sl4a/sl4a.apk
-
     """
     dl, _ = _get_downloader_and_factory(kwargs)
     try:
@@ -922,8 +1136,8 @@
       target = kwargs.get('target', None)
       if not target or not branch:
         raise DevServerError(
-          'Both target and branch must be specified to query for the latest '
-          'Android build.')
+            'Both target and branch must be specified to query for the latest '
+            'Android build.')
       return android_build.BuildAccessor.GetLatestBuildID(target, branch)
 
     try:
@@ -1241,7 +1455,6 @@
 
     Returns:
       A dictionary of IO stats collected by psutil.
-
     """
     return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
             'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
diff --git a/devserver_integration_test.py b/devserver_integration_test.py
index 93b994c..6de4cca 100755
--- a/devserver_integration_test.py
+++ b/devserver_integration_test.py
@@ -9,17 +9,13 @@
 This module is responsible for testing the actual devserver APIs and should be
 run whenever changes are made to the devserver.
 
-Note there are two classes of tests here and they can be run separately.
-
-To just run the short-running "unittests" run:
-  ./devserver_integration_tests.py DevserverUnittests
-
-To just run the longer-running tests, run:
-  ./devserver_integration_tests.py DevserverIntegrationTests
+To run the integration test for devserver:
+  python ./devserver_integration_test.py
 """
 
 from __future__ import print_function
 
+import cros_update_progress
 import devserver_constants
 import json
 import logging
@@ -433,6 +429,45 @@
   2) time. These tests actually download the artifacts needed.
   """
 
+  def testCrosAU(self):
+    """Tests core autotest workflow where we trigger CrOS auto-update.
+
+    It mainly tests the following API:
+      a. 'get_au_status'
+      b. 'handler_cleanup'
+      c. 'kill_au_proc'
+    """
+    host_name = '100.0.0.0'
+    p = subprocess.Popen(['sleep 100'], shell=True)
+    pid = p.pid
+    status = 'updating'
+    progress_tracker = cros_update_progress.AUProgress(host_name, pid)
+    progress_tracker.WriteStatus(status)
+
+    logging.info('Retrieving auto-update status for process %d', pid)
+    response = self._MakeRPC('get_au_status', host_name=host_name, pid=pid)
+    self.assertFalse(json.loads(response)[0])
+    self.assertEqual(json.loads(response)[1], status)
+
+    progress_tracker.WriteStatus(cros_update_progress.FINISHED)
+    logging.info('Mock auto-update process is finished')
+    response = self._MakeRPC('get_au_status', host_name=host_name, pid=pid)
+    self.assertTrue(json.loads(response)[0])
+    self.assertEqual(json.loads(response)[1], cros_update_progress.FINISHED)
+
+    logging.info('Delete auto-update track status file')
+    self.assertTrue(os.path.exists(progress_tracker.track_status_file))
+    self._MakeRPC('handler_cleanup', host_name=host_name, pid=pid)
+    self.assertFalse(os.path.exists(progress_tracker.track_status_file))
+
+    logging.info('Kill the left auto-update processes for host %s', host_name)
+    progress_tracker.WriteStatus(cros_update_progress.FINISHED)
+    response = self._MakeRPC('kill_au_proc', host_name=host_name)
+    self.assertEqual(response, 'True')
+    self.assertFalse(os.path.exists(progress_tracker.track_status_file))
+    self.assertFalse(cros_update_progress.IsProcessAlive(pid))
+
+
   def testStageAndUpdate(self):
     """Tests core autotest workflow where we stage/update with a test payload.