xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 1 | # Copyright (c) 2016 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """An executable function cros-update for auto-update of a CrOS host. |
| 6 | |
| 7 | The reason to create this file is to let devserver to trigger a background |
| 8 | process for CrOS auto-update. Therefore, when devserver service is restarted |
| 9 | sometimes, the CrOS auto-update process is still running and the corresponding |
| 10 | provision task won't claim failure. |
| 11 | |
| 12 | It includes two classes: |
| 13 | a. CrOSUpdateTrigger: |
| 14 | 1. Includes all logics which identify which types of update need to be |
| 15 | performed in the current DUT. |
| 16 | 2. Responsible for write current status of CrOS auto-update process into |
| 17 | progress_tracker. |
| 18 | |
| 19 | b. CrOSAUParser: |
| 20 | 1. Pre-setup the required args for CrOS auto-update. |
| 21 | 2. Parse the input parameters for cmd that runs 'cros_update.py'. |
| 22 | """ |
| 23 | |
| 24 | from __future__ import print_function |
| 25 | |
| 26 | import argparse |
| 27 | import cros_update_logging |
| 28 | import cros_update_progress |
| 29 | import logging |
| 30 | import os |
| 31 | import sys |
xixuan | 28d9907 | 2016-10-06 12:24:16 -0700 | [diff] [blame] | 32 | import traceback |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 33 | |
xixuan | cf58dd3 | 2016-08-24 13:57:06 -0700 | [diff] [blame] | 34 | # only import setup_chromite before chromite import. |
| 35 | import setup_chromite # pylint: disable=unused-import |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 36 | try: |
| 37 | from chromite.lib import auto_updater |
| 38 | from chromite.lib import remote_access |
| 39 | from chromite.lib import timeout_util |
| 40 | except ImportError as e: |
| 41 | logging.debug('chromite cannot be imported: %r', e) |
| 42 | auto_updater = None |
| 43 | remote_access = None |
xixuan | cf58dd3 | 2016-08-24 13:57:06 -0700 | [diff] [blame] | 44 | timeout_util = None |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 45 | |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 46 | # The build channel for recovering host's stateful partition |
| 47 | STABLE_BUILD_CHANNEL = 'stable-channel' |
| 48 | |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 49 | # Timeout for CrOS auto-update process. |
| 50 | CROS_UPDATE_TIMEOUT_MIN = 30 |
| 51 | |
| 52 | # The preserved path in remote device, won't be deleted after rebooting. |
| 53 | CROS_PRESERVED_PATH = ('/mnt/stateful_partition/unencrypted/' |
| 54 | 'preserve/cros-update') |
| 55 | |
| 56 | # Standard error tmeplate to be written into status tracking log. |
xixuan | 28d9907 | 2016-10-06 12:24:16 -0700 | [diff] [blame] | 57 | CROS_ERROR_TEMPLATE = cros_update_progress.ERROR_TAG + ' %s' |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 58 | |
| 59 | |
| 60 | class CrOSAUParser(object): |
| 61 | """Custom command-line options parser for cros-update.""" |
| 62 | def __init__(self): |
| 63 | self.args = sys.argv[1:] |
| 64 | self.parser = argparse.ArgumentParser( |
| 65 | usage='%(prog)s [options] [control-file]') |
| 66 | self.SetupOptions() |
| 67 | self.removed_args = [] |
| 68 | |
| 69 | # parse an empty list of arguments in order to set self.options |
| 70 | # to default values. |
| 71 | self.options = self.parser.parse_args(args=[]) |
| 72 | |
| 73 | def SetupOptions(self): |
| 74 | """Setup options to call cros-update command.""" |
| 75 | self.parser.add_argument('-d', action='store', type=str, |
| 76 | dest='host_name', |
| 77 | help='host_name of a DUT') |
| 78 | self.parser.add_argument('-b', action='store', type=str, |
| 79 | dest='build_name', |
| 80 | help='build name to be auto-updated') |
| 81 | self.parser.add_argument('--static_dir', action='store', type=str, |
| 82 | dest='static_dir', |
| 83 | help='static directory of the devserver') |
| 84 | self.parser.add_argument('--force_update', action='store_true', |
| 85 | dest='force_update', default=False, |
| 86 | help=('force an update even if the version ' |
| 87 | 'installed is the same')) |
| 88 | self.parser.add_argument('--full_update', action='store_true', |
| 89 | dest='full_update', default=False, |
| 90 | help=('force a rootfs update, skip stateful ' |
| 91 | 'update')) |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 92 | self.parser.add_argument('--original_build', action='store', type=str, |
| 93 | dest='original_build', default='', |
| 94 | help=('force stateful update with the same ' |
| 95 | 'version of previous rootfs partition')) |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 96 | |
| 97 | def ParseArgs(self): |
| 98 | """Parse and process command line arguments.""" |
| 99 | # Positional arguments from the end of the command line will be included |
| 100 | # in the list of unknown_args. |
| 101 | self.options, unknown_args = self.parser.parse_known_args() |
| 102 | # Filter out none-positional arguments |
| 103 | while unknown_args and unknown_args[0][0] == '-': |
| 104 | self.removed_args.append(unknown_args.pop(0)) |
| 105 | # Always assume the argument has a value. |
| 106 | if unknown_args: |
| 107 | self.removed_args.append(unknown_args.pop(0)) |
| 108 | if self.removed_args: |
| 109 | logging.warn('Unknown arguments are removed from the options: %s', |
| 110 | self.removed_args) |
| 111 | |
| 112 | |
| 113 | class CrOSUpdateTrigger(object): |
| 114 | """The class for CrOS auto-updater trigger. |
| 115 | |
| 116 | This class is used for running all CrOS auto-update trigger logic. |
| 117 | """ |
| 118 | def __init__(self, host_name, build_name, static_dir, progress_tracker=None, |
xixuan | 3bc974e | 2016-10-18 17:21:43 -0700 | [diff] [blame] | 119 | log_file=None, au_tempdir=None, force_update=False, |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 120 | full_update=False, original_build=None): |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 121 | self.host_name = host_name |
| 122 | self.build_name = build_name |
| 123 | self.static_dir = static_dir |
| 124 | self.progress_tracker = progress_tracker |
| 125 | self.log_file = log_file |
xixuan | 3bc974e | 2016-10-18 17:21:43 -0700 | [diff] [blame] | 126 | self.au_tempdir = au_tempdir |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 127 | self.force_update = force_update |
| 128 | self.full_update = full_update |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 129 | self.original_build = original_build |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 130 | |
| 131 | def _WriteAUStatus(self, content): |
| 132 | if self.progress_tracker: |
| 133 | self.progress_tracker.WriteStatus(content) |
| 134 | |
| 135 | def _StatefulUpdate(self, cros_updater): |
| 136 | """The detailed process in stateful update. |
| 137 | |
| 138 | Args: |
| 139 | cros_updater: The CrOS auto updater for auto-update. |
| 140 | """ |
| 141 | self._WriteAUStatus('pre-setup stateful update') |
| 142 | cros_updater.PreSetupStatefulUpdate() |
| 143 | self._WriteAUStatus('perform stateful update') |
| 144 | cros_updater.UpdateStateful() |
| 145 | self._WriteAUStatus('post-check stateful update') |
| 146 | cros_updater.PostCheckStatefulUpdate() |
| 147 | |
| 148 | def _RootfsUpdate(self, cros_updater): |
| 149 | """The detailed process in rootfs update. |
| 150 | |
| 151 | Args: |
| 152 | cros_updater: The CrOS auto updater for auto-update. |
| 153 | """ |
| 154 | self._WriteAUStatus('transfer rootfs update package') |
| 155 | cros_updater.TransferRootfsUpdate() |
| 156 | self._WriteAUStatus('pre-setup rootfs update') |
| 157 | cros_updater.PreSetupRootfsUpdate() |
| 158 | self._WriteAUStatus('rootfs update') |
| 159 | cros_updater.UpdateRootfs() |
| 160 | self._WriteAUStatus('post-check rootfs update') |
| 161 | cros_updater.PostCheckRootfsUpdate() |
| 162 | |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 163 | def _GetOriginalPayloadDir(self): |
| 164 | """Get the directory of original payload. |
| 165 | |
| 166 | Returns: |
| 167 | The directory of original payload, whose format is like: |
| 168 | 'static/stable-channel/link/3428.210.0' |
| 169 | """ |
| 170 | if self.original_build: |
| 171 | return os.path.join(self.static_dir, '%s/%s' % (STABLE_BUILD_CHANNEL, |
| 172 | self.original_build)) |
| 173 | else: |
| 174 | return None |
| 175 | |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 176 | def TriggerAU(self): |
| 177 | """Execute auto update for cros_host. |
| 178 | |
| 179 | The auto update includes 4 steps: |
| 180 | 1. if devserver cannot run, restore the stateful partition. |
xixuan | 2aca0ac | 2016-07-29 12:02:06 -0700 | [diff] [blame] | 181 | 2. if possible, do stateful update first, but never raise errors, except |
| 182 | for timeout_util.TimeoutError caused by system.signal. |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 183 | 3. If required or stateful_update fails, first do rootfs update, then do |
| 184 | stateful_update. |
| 185 | 4. Post-check for the whole update. |
| 186 | """ |
| 187 | try: |
| 188 | with remote_access.ChromiumOSDeviceHandler( |
| 189 | self.host_name, port=None, |
| 190 | base_dir=CROS_PRESERVED_PATH, |
Luigi Semenzato | 8db8b3c | 2017-01-26 18:02:19 -0800 | [diff] [blame] | 191 | ping=False) as device: |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 192 | |
| 193 | logging.debug('Remote device %s is connected', self.host_name) |
| 194 | payload_dir = os.path.join(self.static_dir, self.build_name) |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 195 | original_payload_dir = self._GetOriginalPayloadDir() |
| 196 | |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 197 | chromeos_AU = auto_updater.ChromiumOSUpdater( |
xixuan | 2a0970a | 2016-08-10 12:12:44 -0700 | [diff] [blame] | 198 | device, self.build_name, payload_dir, |
| 199 | dev_dir=os.path.abspath(os.path.dirname(__file__)), |
xixuan | 3bc974e | 2016-10-18 17:21:43 -0700 | [diff] [blame] | 200 | tempdir=self.au_tempdir, |
xixuan | 2a0970a | 2016-08-10 12:12:44 -0700 | [diff] [blame] | 201 | log_file=self.log_file, |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 202 | original_payload_dir=original_payload_dir, |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 203 | yes=True) |
| 204 | chromeos_AU.CheckPayloads() |
| 205 | |
xixuan | fdfd1e3 | 2016-10-06 14:55:17 -0700 | [diff] [blame] | 206 | version_match = chromeos_AU.PreSetupCrOSUpdate() |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 207 | self._WriteAUStatus('Transfer Devserver/Stateful Update Package') |
| 208 | chromeos_AU.TransferDevServerPackage() |
| 209 | chromeos_AU.TransferStatefulUpdate() |
| 210 | |
| 211 | restore_stateful = chromeos_AU.CheckRestoreStateful() |
| 212 | do_stateful_update = (not self.full_update) and ( |
xixuan | fdfd1e3 | 2016-10-06 14:55:17 -0700 | [diff] [blame] | 213 | version_match and self.force_update) |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 214 | stateful_update_complete = False |
| 215 | logging.debug('Start CrOS update process...') |
| 216 | try: |
| 217 | if restore_stateful: |
| 218 | self._WriteAUStatus('Restore Stateful Partition') |
| 219 | chromeos_AU.RestoreStateful() |
| 220 | stateful_update_complete = True |
| 221 | else: |
| 222 | # Whether to execute stateful update depends on: |
| 223 | # a. full_update=False: No full reimage is required. |
| 224 | # b. The update version is matched to the current version, And |
| 225 | # force_update=True: Update is forced even if the version |
| 226 | # installed is the same. |
| 227 | if do_stateful_update: |
| 228 | self._StatefulUpdate(chromeos_AU) |
| 229 | stateful_update_complete = True |
| 230 | |
xixuan | 2aca0ac | 2016-07-29 12:02:06 -0700 | [diff] [blame] | 231 | except timeout_util.TimeoutError: |
| 232 | raise |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 233 | except Exception as e: |
| 234 | logging.debug('Error happens in stateful update: %r', e) |
| 235 | |
| 236 | # Whether to execute rootfs update depends on: |
| 237 | # a. stateful update is not completed, or completed by |
| 238 | # update action 'restore_stateful'. |
| 239 | # b. force_update=True: Update is forced no matter what the current |
| 240 | # version is. Or, the update version is not matched to the current |
| 241 | # version. |
| 242 | require_rootfs_update = self.force_update or ( |
| 243 | not chromeos_AU.CheckVersion()) |
| 244 | if (not (do_stateful_update and stateful_update_complete) |
| 245 | and require_rootfs_update): |
| 246 | self._RootfsUpdate(chromeos_AU) |
| 247 | self._StatefulUpdate(chromeos_AU) |
| 248 | |
| 249 | self._WriteAUStatus('post-check for CrOS auto-update') |
| 250 | chromeos_AU.PostCheckCrOSUpdate() |
| 251 | self._WriteAUStatus(cros_update_progress.FINISHED) |
| 252 | except Exception as e: |
| 253 | logging.debug('Error happens in CrOS auto-update: %r', e) |
xixuan | 28d9907 | 2016-10-06 12:24:16 -0700 | [diff] [blame] | 254 | self._WriteAUStatus(CROS_ERROR_TEMPLATE % str(traceback.format_exc())) |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 255 | raise |
| 256 | |
| 257 | |
| 258 | def main(): |
| 259 | # Setting logging level |
| 260 | logConfig = cros_update_logging.loggingConfig() |
| 261 | logConfig.ConfigureLogging() |
| 262 | |
| 263 | # Create one cros_update_parser instance for parsing CrOS auto-update cmd. |
| 264 | AU_parser = CrOSAUParser() |
| 265 | try: |
| 266 | AU_parser.ParseArgs() |
| 267 | except Exception as e: |
| 268 | logging.error('Error in Parsing Args: %r', e) |
| 269 | raise |
| 270 | |
| 271 | if len(sys.argv) == 1: |
| 272 | AU_parser.parser.print_help() |
| 273 | sys.exit(1) |
| 274 | |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 275 | options = AU_parser.options |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 276 | |
xixuan | 2a0970a | 2016-08-10 12:12:44 -0700 | [diff] [blame] | 277 | # Use process group id as the unique id in track and log files, since |
| 278 | # os.setsid is executed before the current process is run. |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 279 | pid = os.getpid() |
xixuan | 2a0970a | 2016-08-10 12:12:44 -0700 | [diff] [blame] | 280 | pgid = os.getpgid(pid) |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 281 | |
| 282 | # Setting log files for CrOS auto-update process. |
| 283 | # Log file: file to record every details of CrOS auto-update process. |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 284 | log_file = cros_update_progress.GetExecuteLogFile(options.host_name, pgid) |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 285 | logging.info('Writing executing logs into file: %s', log_file) |
| 286 | logConfig.SetFileHandler(log_file) |
| 287 | |
| 288 | # Create a progress_tracker for tracking CrOS auto-update progress. |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 289 | progress_tracker = cros_update_progress.AUProgress(options.host_name, pgid) |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 290 | |
xixuan | 3bc974e | 2016-10-18 17:21:43 -0700 | [diff] [blame] | 291 | # Create a dir for temporarily storing devserver codes and logs. |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 292 | au_tempdir = cros_update_progress.GetAUTempDirectory(options.host_name, pgid) |
xixuan | 3bc974e | 2016-10-18 17:21:43 -0700 | [diff] [blame] | 293 | |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 294 | # Create cros_update instance to run CrOS auto-update. |
xixuan | ac89ce8 | 2016-11-30 16:48:20 -0800 | [diff] [blame^] | 295 | cros_updater_trigger = CrOSUpdateTrigger( |
| 296 | options.host_name, options.build_name, options.static_dir, |
| 297 | progress_tracker=progress_tracker, |
| 298 | log_file=log_file, |
| 299 | au_tempdir=au_tempdir, |
| 300 | force_update=options.force_update, |
| 301 | full_update=options.full_update, |
| 302 | original_build=options.original_build) |
xixuan | 52c2fba | 2016-05-20 17:02:48 -0700 | [diff] [blame] | 303 | |
| 304 | # Set timeout the cros-update process. |
| 305 | try: |
| 306 | with timeout_util.Timeout(CROS_UPDATE_TIMEOUT_MIN*60): |
| 307 | cros_updater_trigger.TriggerAU() |
| 308 | except timeout_util.TimeoutError as e: |
| 309 | error_msg = ('%s. The CrOS auto-update process is timed out, thus will be ' |
| 310 | 'terminated' % str(e)) |
| 311 | progress_tracker.WriteStatus(CROS_ERROR_TEMPLATE % error_msg) |
| 312 | |
| 313 | |
| 314 | if __name__ == '__main__': |
| 315 | main() |