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.