blob: 10cd6fd5c70eb92930efaf599c0a5adb65b8ff09 [file] [log] [blame]
Amin Hassani86583432019-10-03 10:45:45 -07001# -*- coding: utf-8 -*-
xixuana4f4e712017-05-08 15:17:54 -07002# Copyright 2016 The Chromium OS Authors. All rights reserved.
xixuan52c2fba2016-05-20 17:02:48 -07003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""An executable function cros-update for auto-update of a CrOS host.
7
8The reason to create this file is to let devserver to trigger a background
9process for CrOS auto-update. Therefore, when devserver service is restarted
10sometimes, the CrOS auto-update process is still running and the corresponding
11provision task won't claim failure.
12
13It includes two classes:
14 a. CrOSUpdateTrigger:
15 1. Includes all logics which identify which types of update need to be
16 performed in the current DUT.
17 2. Responsible for write current status of CrOS auto-update process into
18 progress_tracker.
19
20 b. CrOSAUParser:
21 1. Pre-setup the required args for CrOS auto-update.
22 2. Parse the input parameters for cmd that runs 'cros_update.py'.
23"""
24
25from __future__ import print_function
26
xixuan52c2fba2016-05-20 17:02:48 -070027import os
David Rileyef7aad12017-11-24 22:03:21 -080028import re
xixuan52c2fba2016-05-20 17:02:48 -070029import sys
David Riley63820672017-11-02 10:46:42 -070030import time
xixuan28d99072016-10-06 12:24:16 -070031import traceback
Amin Hassani86583432019-10-03 10:45:45 -070032import logging # pylint: disable=cros-logging-import
33
34import cros_update_logging
35import cros_update_progress
xixuan52c2fba2016-05-20 17:02:48 -070036
Amin Hassani86583432019-10-03 10:45:45 -070037import setup_chromite # pylint: disable=unused-import
Amin Hassanie1163962019-10-16 14:54:01 -070038from chromite.lib import auto_updater
39from chromite.lib import commandline
40from chromite.lib import cros_build_lib
41from chromite.lib import remote_access
42from chromite.lib import timeout_util
43
xixuan52c2fba2016-05-20 17:02:48 -070044
xixuanac89ce82016-11-30 16:48:20 -080045# The build channel for recovering host's stateful partition
46STABLE_BUILD_CHANNEL = 'stable-channel'
47
xixuan52c2fba2016-05-20 17:02:48 -070048# Timeout for CrOS auto-update process.
49CROS_UPDATE_TIMEOUT_MIN = 30
50
51# The preserved path in remote device, won't be deleted after rebooting.
52CROS_PRESERVED_PATH = ('/mnt/stateful_partition/unencrypted/'
53 'preserve/cros-update')
54
55# Standard error tmeplate to be written into status tracking log.
xixuan28d99072016-10-06 12:24:16 -070056CROS_ERROR_TEMPLATE = cros_update_progress.ERROR_TAG + ' %s'
xixuan52c2fba2016-05-20 17:02:48 -070057
David Riley63820672017-11-02 10:46:42 -070058# How long after a quick provision fails to wait before falling back to the
59# standard provisioning flow.
60QUICK_PROVISION_FAILURE_DELAY_SEC = 45
61
xixuan27d50442017-08-09 10:38:25 -070062# Setting logging level
63logConfig = cros_update_logging.loggingConfig()
64logConfig.ConfigureLogging()
xixuan52c2fba2016-05-20 17:02:48 -070065
xixuan52c2fba2016-05-20 17:02:48 -070066
67class CrOSUpdateTrigger(object):
68 """The class for CrOS auto-updater trigger.
69
70 This class is used for running all CrOS auto-update trigger logic.
71 """
72 def __init__(self, host_name, build_name, static_dir, progress_tracker=None,
xixuan3bc974e2016-10-18 17:21:43 -070073 log_file=None, au_tempdir=None, force_update=False,
David Haddock20559612017-06-28 22:15:08 -070074 full_update=False, original_build=None, payload_filename=None,
David Riley63820672017-11-02 10:46:42 -070075 clobber_stateful=True, quick_provision=False,
76 devserver_url=None, static_url=None):
xixuan52c2fba2016-05-20 17:02:48 -070077 self.host_name = host_name
78 self.build_name = build_name
79 self.static_dir = static_dir
80 self.progress_tracker = progress_tracker
81 self.log_file = log_file
xixuan3bc974e2016-10-18 17:21:43 -070082 self.au_tempdir = au_tempdir
xixuan52c2fba2016-05-20 17:02:48 -070083 self.force_update = force_update
84 self.full_update = full_update
xixuanac89ce82016-11-30 16:48:20 -080085 self.original_build = original_build
David Haddock90e49442017-04-07 19:14:09 -070086 self.payload_filename = payload_filename
David Haddock20559612017-06-28 22:15:08 -070087 self.clobber_stateful = clobber_stateful
David Riley63820672017-11-02 10:46:42 -070088 self.quick_provision = quick_provision
89 self.devserver_url = devserver_url
90 self.static_url = static_url
xixuan52c2fba2016-05-20 17:02:48 -070091
92 def _WriteAUStatus(self, content):
93 if self.progress_tracker:
94 self.progress_tracker.WriteStatus(content)
95
96 def _StatefulUpdate(self, cros_updater):
97 """The detailed process in stateful update.
98
99 Args:
100 cros_updater: The CrOS auto updater for auto-update.
101 """
102 self._WriteAUStatus('pre-setup stateful update')
103 cros_updater.PreSetupStatefulUpdate()
104 self._WriteAUStatus('perform stateful update')
105 cros_updater.UpdateStateful()
106 self._WriteAUStatus('post-check stateful update')
107 cros_updater.PostCheckStatefulUpdate()
108
109 def _RootfsUpdate(self, cros_updater):
110 """The detailed process in rootfs update.
111
112 Args:
113 cros_updater: The CrOS auto updater for auto-update.
114 """
xixuana4f4e712017-05-08 15:17:54 -0700115 self._WriteAUStatus('Check whether devserver can run before rootfs update')
116 cros_updater.CheckDevserverRun()
xixuan52c2fba2016-05-20 17:02:48 -0700117 self._WriteAUStatus('transfer rootfs update package')
118 cros_updater.TransferRootfsUpdate()
119 self._WriteAUStatus('pre-setup rootfs update')
120 cros_updater.PreSetupRootfsUpdate()
121 self._WriteAUStatus('rootfs update')
122 cros_updater.UpdateRootfs()
123 self._WriteAUStatus('post-check rootfs update')
124 cros_updater.PostCheckRootfsUpdate()
125
xixuanac89ce82016-11-30 16:48:20 -0800126 def _GetOriginalPayloadDir(self):
127 """Get the directory of original payload.
128
129 Returns:
130 The directory of original payload, whose format is like:
131 'static/stable-channel/link/3428.210.0'
132 """
133 if self.original_build:
134 return os.path.join(self.static_dir, '%s/%s' % (STABLE_BUILD_CHANNEL,
135 self.original_build))
136 else:
137 return None
138
David Riley63820672017-11-02 10:46:42 -0700139 def _MakeStatusUrl(self, devserver_url, host_name, pid):
140 """Generates a URL to post auto update status to.
141
142 Args:
143 devserver_url: URL base for devserver RPCs.
144 host_name: Host to post status for.
145 pid: pid of the update process.
146
147 Returns:
148 An unescaped URL.
149 """
150 return '%s/post_au_status?host_name=%s&pid=%d' % (devserver_url, host_name,
151 pid)
152
153 def _QuickProvision(self, device):
154 """Performs a quick provision of device.
155
156 Returns:
David Rileyef7aad12017-11-24 22:03:21 -0800157 A dictionary of extracted key-value pairs returned from the script
158 execution.
159
160 Raises:
161 cros_build_lib.RunCommandError: error executing command or script
162 remote_access.SSHConnectionError: SSH connection error
David Riley63820672017-11-02 10:46:42 -0700163 """
164 pid = os.getpid()
165 pgid = os.getpgid(pid)
166 if self.progress_tracker is None:
167 self.progress_tracker = cros_update_progress.AUProgress(self.host_name,
168 pgid)
169
170 dut_script = '/tmp/quick-provision'
171 status_url = self._MakeStatusUrl(self.devserver_url, self.host_name, pgid)
172 cmd = ('curl -o %s %s && bash '
173 '%s --status_url %s %s %s') % (
174 dut_script, os.path.join(self.static_url, 'quick-provision'),
175 dut_script, cros_build_lib.ShellQuote(status_url),
176 self.build_name, self.static_url
177 )
David Rileyf5941192018-03-01 14:30:38 -0800178 # Quick provision script issues a reboot and might result in the SSH
179 # connection being terminated so set ssh_error_ok so that output can
180 # still be captured.
181 results = device.RunCommand(cmd, log_output=True, capture_output=True,
182 ssh_error_ok=True)
David Rileyef7aad12017-11-24 22:03:21 -0800183 key_re = re.compile(r'^KEYVAL: ([^\d\W]\w*)=(.*)$')
184 matches = [key_re.match(l) for l in results.output.splitlines()]
185 keyvals = {m.group(1): m.group(2) for m in matches if m}
Amin Hassani86583432019-10-03 10:45:45 -0700186 logging.info('DUT returned keyvals: %s', keyvals)
David Rileyf5941192018-03-01 14:30:38 -0800187
188 # If there was an SSH error, check the keyvals to see if it actually
189 # completed and suppress the error if so.
190 if results.returncode == remote_access.SSH_ERROR_CODE:
191 if 'COMPLETED' in keyvals:
192 logging.warning('Quick provision completed with ssh error, ignoring...')
193 else:
194 logging.error('Incomplete quick provision failed with ssh error')
195 raise remote_access.SSHConnectionError(results.error)
196
David Rileyef7aad12017-11-24 22:03:21 -0800197 return keyvals
David Riley63820672017-11-02 10:46:42 -0700198
xixuan52c2fba2016-05-20 17:02:48 -0700199 def TriggerAU(self):
200 """Execute auto update for cros_host.
201
202 The auto update includes 4 steps:
203 1. if devserver cannot run, restore the stateful partition.
xixuan2aca0ac2016-07-29 12:02:06 -0700204 2. if possible, do stateful update first, but never raise errors, except
205 for timeout_util.TimeoutError caused by system.signal.
xixuan52c2fba2016-05-20 17:02:48 -0700206 3. If required or stateful_update fails, first do rootfs update, then do
207 stateful_update.
208 4. Post-check for the whole update.
209 """
210 try:
211 with remote_access.ChromiumOSDeviceHandler(
212 self.host_name, port=None,
213 base_dir=CROS_PRESERVED_PATH,
Luigi Semenzato8db8b3c2017-01-26 18:02:19 -0800214 ping=False) as device:
xixuan52c2fba2016-05-20 17:02:48 -0700215
216 logging.debug('Remote device %s is connected', self.host_name)
217 payload_dir = os.path.join(self.static_dir, self.build_name)
xixuanac89ce82016-11-30 16:48:20 -0800218 original_payload_dir = self._GetOriginalPayloadDir()
219
xixuan52c2fba2016-05-20 17:02:48 -0700220 chromeos_AU = auto_updater.ChromiumOSUpdater(
xixuan2a0970a2016-08-10 12:12:44 -0700221 device, self.build_name, payload_dir,
222 dev_dir=os.path.abspath(os.path.dirname(__file__)),
xixuan3bc974e2016-10-18 17:21:43 -0700223 tempdir=self.au_tempdir,
xixuan2a0970a2016-08-10 12:12:44 -0700224 log_file=self.log_file,
xixuanac89ce82016-11-30 16:48:20 -0800225 original_payload_dir=original_payload_dir,
David Haddock90e49442017-04-07 19:14:09 -0700226 yes=True,
David Haddock20559612017-06-28 22:15:08 -0700227 payload_filename=self.payload_filename,
228 clobber_stateful=self.clobber_stateful)
xixuan52c2fba2016-05-20 17:02:48 -0700229
David Riley63820672017-11-02 10:46:42 -0700230 # Allow fall back if the quick provision does not succeed.
231 invoke_autoupdate = True
xixuan52c2fba2016-05-20 17:02:48 -0700232
David Riley6c467252017-11-30 16:39:11 -0800233 if (self.quick_provision and self.clobber_stateful and
234 not self.full_update):
David Riley63820672017-11-02 10:46:42 -0700235 try:
236 logging.debug('Start CrOS quick provision process...')
237 self._WriteAUStatus('Start Quick Provision')
David Riley6c467252017-11-30 16:39:11 -0800238 keyvals = self._QuickProvision(device)
David Riley63820672017-11-02 10:46:42 -0700239 logging.debug('Start CrOS check process...')
David Riley8a7ed5c2018-02-22 11:16:47 -0800240 self._WriteAUStatus('Finish Quick Provision, reboot')
241 chromeos_AU.AwaitReboot(keyvals.get('BOOT_ID'))
David Riley63820672017-11-02 10:46:42 -0700242 self._WriteAUStatus('Finish Quick Provision, post-check')
David Riley8a7ed5c2018-02-22 11:16:47 -0800243 chromeos_AU.PostCheckCrOSUpdate()
David Riley63820672017-11-02 10:46:42 -0700244 self._WriteAUStatus(cros_update_progress.FINISHED)
245 invoke_autoupdate = False
246 except (cros_build_lib.RunCommandError,
David Riley8a7ed5c2018-02-22 11:16:47 -0800247 remote_access.SSHConnectionError,
248 auto_updater.RebootVerificationError) as e:
David Rileyf5941192018-03-01 14:30:38 -0800249 logging.warning(
250 'Error during quick provision, falling back to legacy: %s: %s',
251 type(e).__name__, e)
David Riley63820672017-11-02 10:46:42 -0700252 time.sleep(QUICK_PROVISION_FAILURE_DELAY_SEC)
253
254 if invoke_autoupdate:
255 chromeos_AU.CheckPayloads()
256
257 version_match = chromeos_AU.PreSetupCrOSUpdate()
258 self._WriteAUStatus('Transfer Devserver/Stateful Update Package')
259 chromeos_AU.TransferDevServerPackage()
260 chromeos_AU.TransferStatefulUpdate()
261
262 restore_stateful = chromeos_AU.CheckRestoreStateful()
263 do_stateful_update = (not self.full_update) and (
264 version_match and self.force_update)
265 stateful_update_complete = False
266 logging.debug('Start CrOS update process...')
267 try:
268 if restore_stateful:
269 self._WriteAUStatus('Restore Stateful Partition')
270 chromeos_AU.RestoreStateful()
xixuan52c2fba2016-05-20 17:02:48 -0700271 stateful_update_complete = True
David Riley63820672017-11-02 10:46:42 -0700272 else:
273 # Whether to execute stateful update depends on:
274 # a. full_update=False: No full reimage is required.
275 # b. The update version is matched to the current version, And
276 # force_update=True: Update is forced even if the version
277 # installed is the same.
278 if do_stateful_update:
279 self._StatefulUpdate(chromeos_AU)
280 stateful_update_complete = True
xixuan52c2fba2016-05-20 17:02:48 -0700281
David Riley63820672017-11-02 10:46:42 -0700282 except timeout_util.TimeoutError:
283 raise
284 except Exception as e:
285 logging.debug('Error happens in stateful update: %r', e)
xixuan52c2fba2016-05-20 17:02:48 -0700286
David Riley63820672017-11-02 10:46:42 -0700287 # Whether to execute rootfs update depends on:
288 # a. stateful update is not completed, or completed by
289 # update action 'restore_stateful'.
290 # b. force_update=True: Update is forced no matter what the current
291 # version is. Or, the update version is not matched to the current
292 # version.
293 require_rootfs_update = self.force_update or (
294 not chromeos_AU.CheckVersion())
295 if (not (do_stateful_update and stateful_update_complete)
296 and require_rootfs_update):
297 self._RootfsUpdate(chromeos_AU)
298 self._StatefulUpdate(chromeos_AU)
xixuan52c2fba2016-05-20 17:02:48 -0700299
David Riley63820672017-11-02 10:46:42 -0700300 self._WriteAUStatus('post-check for CrOS auto-update')
301 chromeos_AU.PostCheckCrOSUpdate()
302 self._WriteAUStatus(cros_update_progress.FINISHED)
David Rileyf5941192018-03-01 14:30:38 -0800303
304 logging.debug('Provision successfully completed (%s)',
305 'legacy' if invoke_autoupdate else 'quick provision')
xixuan52c2fba2016-05-20 17:02:48 -0700306 except Exception as e:
307 logging.debug('Error happens in CrOS auto-update: %r', e)
xixuan28d99072016-10-06 12:24:16 -0700308 self._WriteAUStatus(CROS_ERROR_TEMPLATE % str(traceback.format_exc()))
xixuan52c2fba2016-05-20 17:02:48 -0700309 raise
310
311
Amin Hassanie1163962019-10-16 14:54:01 -0700312def ParseArguments(argv):
313 """Returns a namespace for the CLI arguments."""
314 parser = commandline.ArgumentParser(description=__doc__)
315 parser.add_argument('-d', action='store', type=str, dest='host_name',
316 help='host_name of a DUT')
317 parser.add_argument('-b', action='store', type=str, dest='build_name',
318 help='build name to be auto-updated')
319 parser.add_argument('--static_dir', action='store', type='path',
320 help='static directory of the devserver')
321 parser.add_argument('--force_update', action='store_true', default=False,
322 help=('force an update even if the version installed is '
323 'the same'))
324 parser.add_argument('--full_update', action='store_true', default=False,
325 help='force a rootfs update, skip stateful update')
326 parser.add_argument('--original_build', action='store', type=str, default='',
327 help=('force stateful update with the same version of '
328 'previous rootfs partition'))
329 parser.add_argument('--payload_filename', action='store', type=str,
330 default=None, help='A custom payload filename')
331 parser.add_argument('--clobber_stateful', action='store_true', default=False,
332 help='Whether to clobber stateful')
333 parser.add_argument('--quick_provision', action='store_true', default=False,
334 help='Whether to attempt quick provisioning path')
335 parser.add_argument('--devserver_url', action='store', type=str, default=None,
336 help='Devserver URL base for RPCs')
337 parser.add_argument('--static_url', action='store', type=str, default=None,
338 help='Devserver URL base for static files')
xixuan52c2fba2016-05-20 17:02:48 -0700339
Amin Hassanie1163962019-10-16 14:54:01 -0700340 opts = parser.parse_args(argv)
341 opts.Freeze()
xixuan52c2fba2016-05-20 17:02:48 -0700342
Amin Hassanie1163962019-10-16 14:54:01 -0700343 return opts
344
345
346def main(argv):
347 options = ParseArguments(argv)
xixuan52c2fba2016-05-20 17:02:48 -0700348
xixuan2a0970a2016-08-10 12:12:44 -0700349 # Use process group id as the unique id in track and log files, since
350 # os.setsid is executed before the current process is run.
xixuan52c2fba2016-05-20 17:02:48 -0700351 pid = os.getpid()
xixuan2a0970a2016-08-10 12:12:44 -0700352 pgid = os.getpgid(pid)
xixuan52c2fba2016-05-20 17:02:48 -0700353
354 # Setting log files for CrOS auto-update process.
355 # Log file: file to record every details of CrOS auto-update process.
xixuanac89ce82016-11-30 16:48:20 -0800356 log_file = cros_update_progress.GetExecuteLogFile(options.host_name, pgid)
xixuan52c2fba2016-05-20 17:02:48 -0700357 logging.info('Writing executing logs into file: %s', log_file)
358 logConfig.SetFileHandler(log_file)
359
360 # Create a progress_tracker for tracking CrOS auto-update progress.
xixuanac89ce82016-11-30 16:48:20 -0800361 progress_tracker = cros_update_progress.AUProgress(options.host_name, pgid)
xixuan52c2fba2016-05-20 17:02:48 -0700362
xixuan3bc974e2016-10-18 17:21:43 -0700363 # Create a dir for temporarily storing devserver codes and logs.
xixuanac89ce82016-11-30 16:48:20 -0800364 au_tempdir = cros_update_progress.GetAUTempDirectory(options.host_name, pgid)
xixuan3bc974e2016-10-18 17:21:43 -0700365
xixuan52c2fba2016-05-20 17:02:48 -0700366 # Create cros_update instance to run CrOS auto-update.
xixuanac89ce82016-11-30 16:48:20 -0800367 cros_updater_trigger = CrOSUpdateTrigger(
368 options.host_name, options.build_name, options.static_dir,
369 progress_tracker=progress_tracker,
370 log_file=log_file,
371 au_tempdir=au_tempdir,
372 force_update=options.force_update,
373 full_update=options.full_update,
David Haddock90e49442017-04-07 19:14:09 -0700374 original_build=options.original_build,
David Haddock20559612017-06-28 22:15:08 -0700375 payload_filename=options.payload_filename,
David Riley63820672017-11-02 10:46:42 -0700376 clobber_stateful=options.clobber_stateful,
377 quick_provision=options.quick_provision,
378 devserver_url=options.devserver_url,
379 static_url=options.static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700380
381 # Set timeout the cros-update process.
382 try:
383 with timeout_util.Timeout(CROS_UPDATE_TIMEOUT_MIN*60):
384 cros_updater_trigger.TriggerAU()
385 except timeout_util.TimeoutError as e:
386 error_msg = ('%s. The CrOS auto-update process is timed out, thus will be '
387 'terminated' % str(e))
388 progress_tracker.WriteStatus(CROS_ERROR_TEMPLATE % error_msg)
389
390
391if __name__ == '__main__':
Amin Hassani78520ae2019-10-29 13:26:51 -0700392 main(sys.argv[1:])