blob: 6f919655411e0d087839421328b32b6dcfc2b6b9 [file] [log] [blame]
xixuana4f4e712017-05-08 15:17:54 -07001# Copyright 2016 The Chromium OS Authors. All rights reserved.
xixuan52c2fba2016-05-20 17:02:48 -07002# 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
7The reason to create this file is to let devserver to trigger a background
8process for CrOS auto-update. Therefore, when devserver service is restarted
9sometimes, the CrOS auto-update process is still running and the corresponding
10provision task won't claim failure.
11
12It 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
24from __future__ import print_function
25
26import argparse
27import cros_update_logging
28import cros_update_progress
David Riley63820672017-11-02 10:46:42 -070029import logging # pylint: disable=cros-logging-import
xixuan52c2fba2016-05-20 17:02:48 -070030import os
31import sys
David Riley63820672017-11-02 10:46:42 -070032import time
xixuan28d99072016-10-06 12:24:16 -070033import traceback
xixuan52c2fba2016-05-20 17:02:48 -070034
xixuancf58dd32016-08-24 13:57:06 -070035# only import setup_chromite before chromite import.
36import setup_chromite # pylint: disable=unused-import
xixuan52c2fba2016-05-20 17:02:48 -070037try:
38 from chromite.lib import auto_updater
David Riley63820672017-11-02 10:46:42 -070039 from chromite.lib import cros_build_lib
xixuan52c2fba2016-05-20 17:02:48 -070040 from chromite.lib import remote_access
41 from chromite.lib import timeout_util
42except ImportError as e:
43 logging.debug('chromite cannot be imported: %r', e)
44 auto_updater = None
45 remote_access = None
xixuancf58dd32016-08-24 13:57:06 -070046 timeout_util = None
xixuan52c2fba2016-05-20 17:02:48 -070047
xixuanac89ce82016-11-30 16:48:20 -080048# The build channel for recovering host's stateful partition
49STABLE_BUILD_CHANNEL = 'stable-channel'
50
xixuan52c2fba2016-05-20 17:02:48 -070051# Timeout for CrOS auto-update process.
52CROS_UPDATE_TIMEOUT_MIN = 30
53
54# The preserved path in remote device, won't be deleted after rebooting.
55CROS_PRESERVED_PATH = ('/mnt/stateful_partition/unencrypted/'
56 'preserve/cros-update')
57
58# Standard error tmeplate to be written into status tracking log.
xixuan28d99072016-10-06 12:24:16 -070059CROS_ERROR_TEMPLATE = cros_update_progress.ERROR_TAG + ' %s'
xixuan52c2fba2016-05-20 17:02:48 -070060
David Riley63820672017-11-02 10:46:42 -070061# How long after a quick provision fails to wait before falling back to the
62# standard provisioning flow.
63QUICK_PROVISION_FAILURE_DELAY_SEC = 45
64
xixuan27d50442017-08-09 10:38:25 -070065# Setting logging level
66logConfig = cros_update_logging.loggingConfig()
67logConfig.ConfigureLogging()
xixuan52c2fba2016-05-20 17:02:48 -070068
69class CrOSAUParser(object):
70 """Custom command-line options parser for cros-update."""
71 def __init__(self):
72 self.args = sys.argv[1:]
73 self.parser = argparse.ArgumentParser(
74 usage='%(prog)s [options] [control-file]')
75 self.SetupOptions()
76 self.removed_args = []
77
78 # parse an empty list of arguments in order to set self.options
79 # to default values.
80 self.options = self.parser.parse_args(args=[])
81
82 def SetupOptions(self):
83 """Setup options to call cros-update command."""
84 self.parser.add_argument('-d', action='store', type=str,
85 dest='host_name',
86 help='host_name of a DUT')
87 self.parser.add_argument('-b', action='store', type=str,
88 dest='build_name',
89 help='build name to be auto-updated')
90 self.parser.add_argument('--static_dir', action='store', type=str,
xixuan52c2fba2016-05-20 17:02:48 -070091 help='static directory of the devserver')
92 self.parser.add_argument('--force_update', action='store_true',
David Riley63820672017-11-02 10:46:42 -070093 default=False,
xixuan52c2fba2016-05-20 17:02:48 -070094 help=('force an update even if the version '
95 'installed is the same'))
96 self.parser.add_argument('--full_update', action='store_true',
David Riley63820672017-11-02 10:46:42 -070097 default=False,
98 help='force a rootfs update, skip stateful update')
xixuanac89ce82016-11-30 16:48:20 -080099 self.parser.add_argument('--original_build', action='store', type=str,
David Riley63820672017-11-02 10:46:42 -0700100 default='',
xixuanac89ce82016-11-30 16:48:20 -0800101 help=('force stateful update with the same '
102 'version of previous rootfs partition'))
David Haddock90e49442017-04-07 19:14:09 -0700103 self.parser.add_argument('--payload_filename', action='store', type=str,
David Riley63820672017-11-02 10:46:42 -0700104 default=None, help='A custom payload filename')
David Haddock20559612017-06-28 22:15:08 -0700105 self.parser.add_argument('--clobber_stateful', action='store_true',
David Riley63820672017-11-02 10:46:42 -0700106 default=False, help='Whether to clobber stateful')
107 self.parser.add_argument('--quick_provision', action='store_true',
108 default=False,
109 help='Whether to attempt quick provisioning path')
110 self.parser.add_argument('--devserver_url', action='store', type=str,
111 default=None, help='Devserver URL base for RPCs')
112 self.parser.add_argument('--static_url', action='store', type=str,
113 default=None,
114 help='Devserver URL base for static files')
xixuan52c2fba2016-05-20 17:02:48 -0700115
116 def ParseArgs(self):
117 """Parse and process command line arguments."""
118 # Positional arguments from the end of the command line will be included
119 # in the list of unknown_args.
120 self.options, unknown_args = self.parser.parse_known_args()
121 # Filter out none-positional arguments
122 while unknown_args and unknown_args[0][0] == '-':
123 self.removed_args.append(unknown_args.pop(0))
124 # Always assume the argument has a value.
125 if unknown_args:
126 self.removed_args.append(unknown_args.pop(0))
127 if self.removed_args:
128 logging.warn('Unknown arguments are removed from the options: %s',
129 self.removed_args)
130
131
132class CrOSUpdateTrigger(object):
133 """The class for CrOS auto-updater trigger.
134
135 This class is used for running all CrOS auto-update trigger logic.
136 """
137 def __init__(self, host_name, build_name, static_dir, progress_tracker=None,
xixuan3bc974e2016-10-18 17:21:43 -0700138 log_file=None, au_tempdir=None, force_update=False,
David Haddock20559612017-06-28 22:15:08 -0700139 full_update=False, original_build=None, payload_filename=None,
David Riley63820672017-11-02 10:46:42 -0700140 clobber_stateful=True, quick_provision=False,
141 devserver_url=None, static_url=None):
xixuan52c2fba2016-05-20 17:02:48 -0700142 self.host_name = host_name
143 self.build_name = build_name
144 self.static_dir = static_dir
145 self.progress_tracker = progress_tracker
146 self.log_file = log_file
xixuan3bc974e2016-10-18 17:21:43 -0700147 self.au_tempdir = au_tempdir
xixuan52c2fba2016-05-20 17:02:48 -0700148 self.force_update = force_update
149 self.full_update = full_update
xixuanac89ce82016-11-30 16:48:20 -0800150 self.original_build = original_build
David Haddock90e49442017-04-07 19:14:09 -0700151 self.payload_filename = payload_filename
David Haddock20559612017-06-28 22:15:08 -0700152 self.clobber_stateful = clobber_stateful
David Riley63820672017-11-02 10:46:42 -0700153 self.quick_provision = quick_provision
154 self.devserver_url = devserver_url
155 self.static_url = static_url
xixuan52c2fba2016-05-20 17:02:48 -0700156
157 def _WriteAUStatus(self, content):
158 if self.progress_tracker:
159 self.progress_tracker.WriteStatus(content)
160
161 def _StatefulUpdate(self, cros_updater):
162 """The detailed process in stateful update.
163
164 Args:
165 cros_updater: The CrOS auto updater for auto-update.
166 """
167 self._WriteAUStatus('pre-setup stateful update')
168 cros_updater.PreSetupStatefulUpdate()
169 self._WriteAUStatus('perform stateful update')
170 cros_updater.UpdateStateful()
171 self._WriteAUStatus('post-check stateful update')
172 cros_updater.PostCheckStatefulUpdate()
173
174 def _RootfsUpdate(self, cros_updater):
175 """The detailed process in rootfs update.
176
177 Args:
178 cros_updater: The CrOS auto updater for auto-update.
179 """
xixuana4f4e712017-05-08 15:17:54 -0700180 self._WriteAUStatus('Check whether devserver can run before rootfs update')
181 cros_updater.CheckDevserverRun()
xixuan52c2fba2016-05-20 17:02:48 -0700182 self._WriteAUStatus('transfer rootfs update package')
183 cros_updater.TransferRootfsUpdate()
184 self._WriteAUStatus('pre-setup rootfs update')
185 cros_updater.PreSetupRootfsUpdate()
186 self._WriteAUStatus('rootfs update')
187 cros_updater.UpdateRootfs()
188 self._WriteAUStatus('post-check rootfs update')
189 cros_updater.PostCheckRootfsUpdate()
190
xixuanac89ce82016-11-30 16:48:20 -0800191 def _GetOriginalPayloadDir(self):
192 """Get the directory of original payload.
193
194 Returns:
195 The directory of original payload, whose format is like:
196 'static/stable-channel/link/3428.210.0'
197 """
198 if self.original_build:
199 return os.path.join(self.static_dir, '%s/%s' % (STABLE_BUILD_CHANNEL,
200 self.original_build))
201 else:
202 return None
203
David Riley63820672017-11-02 10:46:42 -0700204 def _MakeStatusUrl(self, devserver_url, host_name, pid):
205 """Generates a URL to post auto update status to.
206
207 Args:
208 devserver_url: URL base for devserver RPCs.
209 host_name: Host to post status for.
210 pid: pid of the update process.
211
212 Returns:
213 An unescaped URL.
214 """
215 return '%s/post_au_status?host_name=%s&pid=%d' % (devserver_url, host_name,
216 pid)
217
218 def _QuickProvision(self, device):
219 """Performs a quick provision of device.
220
221 Returns:
222 A CommandResult of the invocation.
223 """
224 pid = os.getpid()
225 pgid = os.getpgid(pid)
226 if self.progress_tracker is None:
227 self.progress_tracker = cros_update_progress.AUProgress(self.host_name,
228 pgid)
229
230 dut_script = '/tmp/quick-provision'
231 status_url = self._MakeStatusUrl(self.devserver_url, self.host_name, pgid)
232 cmd = ('curl -o %s %s && bash '
233 '%s --status_url %s %s %s') % (
234 dut_script, os.path.join(self.static_url, 'quick-provision'),
235 dut_script, cros_build_lib.ShellQuote(status_url),
236 self.build_name, self.static_url
237 )
238 return device.RunCommand(cmd, log_output=True)
239
xixuan52c2fba2016-05-20 17:02:48 -0700240 def TriggerAU(self):
241 """Execute auto update for cros_host.
242
243 The auto update includes 4 steps:
244 1. if devserver cannot run, restore the stateful partition.
xixuan2aca0ac2016-07-29 12:02:06 -0700245 2. if possible, do stateful update first, but never raise errors, except
246 for timeout_util.TimeoutError caused by system.signal.
xixuan52c2fba2016-05-20 17:02:48 -0700247 3. If required or stateful_update fails, first do rootfs update, then do
248 stateful_update.
249 4. Post-check for the whole update.
250 """
251 try:
252 with remote_access.ChromiumOSDeviceHandler(
253 self.host_name, port=None,
254 base_dir=CROS_PRESERVED_PATH,
Luigi Semenzato8db8b3c2017-01-26 18:02:19 -0800255 ping=False) as device:
xixuan52c2fba2016-05-20 17:02:48 -0700256
257 logging.debug('Remote device %s is connected', self.host_name)
258 payload_dir = os.path.join(self.static_dir, self.build_name)
xixuanac89ce82016-11-30 16:48:20 -0800259 original_payload_dir = self._GetOriginalPayloadDir()
260
xixuan52c2fba2016-05-20 17:02:48 -0700261 chromeos_AU = auto_updater.ChromiumOSUpdater(
xixuan2a0970a2016-08-10 12:12:44 -0700262 device, self.build_name, payload_dir,
263 dev_dir=os.path.abspath(os.path.dirname(__file__)),
xixuan3bc974e2016-10-18 17:21:43 -0700264 tempdir=self.au_tempdir,
xixuan2a0970a2016-08-10 12:12:44 -0700265 log_file=self.log_file,
xixuanac89ce82016-11-30 16:48:20 -0800266 original_payload_dir=original_payload_dir,
David Haddock90e49442017-04-07 19:14:09 -0700267 yes=True,
David Haddock20559612017-06-28 22:15:08 -0700268 payload_filename=self.payload_filename,
269 clobber_stateful=self.clobber_stateful)
xixuan52c2fba2016-05-20 17:02:48 -0700270
David Riley63820672017-11-02 10:46:42 -0700271 # Allow fall back if the quick provision does not succeed.
272 invoke_autoupdate = True
xixuan52c2fba2016-05-20 17:02:48 -0700273
David Riley63820672017-11-02 10:46:42 -0700274 if self.quick_provision:
275 try:
276 logging.debug('Start CrOS quick provision process...')
277 self._WriteAUStatus('Start Quick Provision')
278 self._QuickProvision(device)
279 logging.debug('Start CrOS check process...')
280 self._WriteAUStatus('Finish Quick Provision, post-check')
281 chromeos_AU.PostCheckCrOSUpdate()
282 self._WriteAUStatus(cros_update_progress.FINISHED)
283 invoke_autoupdate = False
284 except (cros_build_lib.RunCommandError,
285 remote_access.SSHConnectionError) as e:
286 logging.warning('Error during quick provision, falling back: %s', e)
287 time.sleep(QUICK_PROVISION_FAILURE_DELAY_SEC)
288
289 if invoke_autoupdate:
290 chromeos_AU.CheckPayloads()
291
292 version_match = chromeos_AU.PreSetupCrOSUpdate()
293 self._WriteAUStatus('Transfer Devserver/Stateful Update Package')
294 chromeos_AU.TransferDevServerPackage()
295 chromeos_AU.TransferStatefulUpdate()
296
297 restore_stateful = chromeos_AU.CheckRestoreStateful()
298 do_stateful_update = (not self.full_update) and (
299 version_match and self.force_update)
300 stateful_update_complete = False
301 logging.debug('Start CrOS update process...')
302 try:
303 if restore_stateful:
304 self._WriteAUStatus('Restore Stateful Partition')
305 chromeos_AU.RestoreStateful()
xixuan52c2fba2016-05-20 17:02:48 -0700306 stateful_update_complete = True
David Riley63820672017-11-02 10:46:42 -0700307 else:
308 # Whether to execute stateful update depends on:
309 # a. full_update=False: No full reimage is required.
310 # b. The update version is matched to the current version, And
311 # force_update=True: Update is forced even if the version
312 # installed is the same.
313 if do_stateful_update:
314 self._StatefulUpdate(chromeos_AU)
315 stateful_update_complete = True
xixuan52c2fba2016-05-20 17:02:48 -0700316
David Riley63820672017-11-02 10:46:42 -0700317 except timeout_util.TimeoutError:
318 raise
319 except Exception as e:
320 logging.debug('Error happens in stateful update: %r', e)
xixuan52c2fba2016-05-20 17:02:48 -0700321
David Riley63820672017-11-02 10:46:42 -0700322 # Whether to execute rootfs update depends on:
323 # a. stateful update is not completed, or completed by
324 # update action 'restore_stateful'.
325 # b. force_update=True: Update is forced no matter what the current
326 # version is. Or, the update version is not matched to the current
327 # version.
328 require_rootfs_update = self.force_update or (
329 not chromeos_AU.CheckVersion())
330 if (not (do_stateful_update and stateful_update_complete)
331 and require_rootfs_update):
332 self._RootfsUpdate(chromeos_AU)
333 self._StatefulUpdate(chromeos_AU)
xixuan52c2fba2016-05-20 17:02:48 -0700334
David Riley63820672017-11-02 10:46:42 -0700335 self._WriteAUStatus('post-check for CrOS auto-update')
336 chromeos_AU.PostCheckCrOSUpdate()
337 self._WriteAUStatus(cros_update_progress.FINISHED)
xixuan52c2fba2016-05-20 17:02:48 -0700338 except Exception as e:
339 logging.debug('Error happens in CrOS auto-update: %r', e)
xixuan28d99072016-10-06 12:24:16 -0700340 self._WriteAUStatus(CROS_ERROR_TEMPLATE % str(traceback.format_exc()))
xixuan52c2fba2016-05-20 17:02:48 -0700341 raise
342
343
344def main():
xixuan52c2fba2016-05-20 17:02:48 -0700345 # Create one cros_update_parser instance for parsing CrOS auto-update cmd.
346 AU_parser = CrOSAUParser()
347 try:
348 AU_parser.ParseArgs()
349 except Exception as e:
350 logging.error('Error in Parsing Args: %r', e)
351 raise
352
353 if len(sys.argv) == 1:
354 AU_parser.parser.print_help()
355 sys.exit(1)
356
xixuanac89ce82016-11-30 16:48:20 -0800357 options = AU_parser.options
xixuan52c2fba2016-05-20 17:02:48 -0700358
xixuan2a0970a2016-08-10 12:12:44 -0700359 # Use process group id as the unique id in track and log files, since
360 # os.setsid is executed before the current process is run.
xixuan52c2fba2016-05-20 17:02:48 -0700361 pid = os.getpid()
xixuan2a0970a2016-08-10 12:12:44 -0700362 pgid = os.getpgid(pid)
xixuan52c2fba2016-05-20 17:02:48 -0700363
364 # Setting log files for CrOS auto-update process.
365 # Log file: file to record every details of CrOS auto-update process.
xixuanac89ce82016-11-30 16:48:20 -0800366 log_file = cros_update_progress.GetExecuteLogFile(options.host_name, pgid)
xixuan52c2fba2016-05-20 17:02:48 -0700367 logging.info('Writing executing logs into file: %s', log_file)
368 logConfig.SetFileHandler(log_file)
369
370 # Create a progress_tracker for tracking CrOS auto-update progress.
xixuanac89ce82016-11-30 16:48:20 -0800371 progress_tracker = cros_update_progress.AUProgress(options.host_name, pgid)
xixuan52c2fba2016-05-20 17:02:48 -0700372
xixuan3bc974e2016-10-18 17:21:43 -0700373 # Create a dir for temporarily storing devserver codes and logs.
xixuanac89ce82016-11-30 16:48:20 -0800374 au_tempdir = cros_update_progress.GetAUTempDirectory(options.host_name, pgid)
xixuan3bc974e2016-10-18 17:21:43 -0700375
xixuan52c2fba2016-05-20 17:02:48 -0700376 # Create cros_update instance to run CrOS auto-update.
xixuanac89ce82016-11-30 16:48:20 -0800377 cros_updater_trigger = CrOSUpdateTrigger(
378 options.host_name, options.build_name, options.static_dir,
379 progress_tracker=progress_tracker,
380 log_file=log_file,
381 au_tempdir=au_tempdir,
382 force_update=options.force_update,
383 full_update=options.full_update,
David Haddock90e49442017-04-07 19:14:09 -0700384 original_build=options.original_build,
David Haddock20559612017-06-28 22:15:08 -0700385 payload_filename=options.payload_filename,
David Riley63820672017-11-02 10:46:42 -0700386 clobber_stateful=options.clobber_stateful,
387 quick_provision=options.quick_provision,
388 devserver_url=options.devserver_url,
389 static_url=options.static_url)
xixuan52c2fba2016-05-20 17:02:48 -0700390
391 # Set timeout the cros-update process.
392 try:
393 with timeout_util.Timeout(CROS_UPDATE_TIMEOUT_MIN*60):
394 cros_updater_trigger.TriggerAU()
395 except timeout_util.TimeoutError as e:
396 error_msg = ('%s. The CrOS auto-update process is timed out, thus will be '
397 'terminated' % str(e))
398 progress_tracker.WriteStatus(CROS_ERROR_TEMPLATE % error_msg)
399
400
401if __name__ == '__main__':
402 main()