blob: 4d6186e1d904169c95ccd78770f08e8fcb06723a [file] [log] [blame]
Chris Sosa5e4246b2012-05-22 18:05:22 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Sean O'Connor5346e4e2010-08-12 18:49:24 +02002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import httplib
6import logging
Chris Sosa77556d82012-04-05 15:23:14 -07007import multiprocessing
Dale Curtis5c32c722011-05-04 19:24:23 -07008import os
Sean O'Connor5346e4e2010-08-12 18:49:24 +02009import re
Sean O'Connor5346e4e2010-08-12 18:49:24 +020010import urlparse
11
Dale Curtis5c32c722011-05-04 19:24:23 -070012from autotest_lib.client.common_lib import error, global_config
Sean O'Connor5346e4e2010-08-12 18:49:24 +020013
Dale Curtis5c32c722011-05-04 19:24:23 -070014# Local stateful update path is relative to the CrOS source directory.
15LOCAL_STATEFUL_UPDATE_PATH = 'src/platform/dev/stateful_update'
Chris Sosaa3ac2152012-05-23 22:23:13 -070016LOCAL_CHROOT_STATEFUL_UPDATE_PATH = '/usr/bin/stateful_update'
Dale Curtis5c32c722011-05-04 19:24:23 -070017REMOTE_STATEUL_UPDATE_PATH = '/usr/local/bin/stateful_update'
18STATEFUL_UPDATE = '/tmp/stateful_update'
Sean O'Connor5346e4e2010-08-12 18:49:24 +020019UPDATER_BIN = '/usr/bin/update_engine_client'
20UPDATER_IDLE = 'UPDATE_STATUS_IDLE'
Sean Oc053dfe2010-08-23 18:22:26 +020021UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
Darin Petkov7d572992010-09-23 10:11:05 -070022UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed'
Dale Curtis1e973182011-07-12 18:21:36 -070023UPDATER_LOGS = '/var/log/messages /var/log/update_engine'
Sean O'Connor5346e4e2010-08-12 18:49:24 +020024
25
26class ChromiumOSError(error.InstallError):
27 """Generic error for ChromiumOS-specific exceptions."""
28 pass
29
30
Chris Sosa77556d82012-04-05 15:23:14 -070031class RootFSUpdateError(ChromiumOSError):
32 """Raised when the RootFS fails to update."""
33 pass
34
35
36class StatefulUpdateError(ChromiumOSError):
37 """Raised when the stateful partition fails to update."""
38 pass
39
40
Sean O'Connor5346e4e2010-08-12 18:49:24 +020041def url_to_version(update_url):
Dan Shi0f466e82013-02-22 15:44:58 -080042 """Return the version based on update_url.
43
44 @param update_url: url to the image to update to.
45
46 """
Dale Curtisddfdb942011-07-14 13:59:24 -070047 # The Chrome OS version is generally the last element in the URL. The only
48 # exception is delta update URLs, which are rooted under the version; e.g.,
49 # http://.../update/.../0.14.755.0/au/0.14.754.0. In this case we want to
50 # strip off the au section of the path before reading the version.
51 return re.sub(
52 '/au/.*', '', urlparse.urlparse(update_url).path).split('/')[-1]
Sean O'Connor5346e4e2010-08-12 18:49:24 +020053
54
Scott Zawalskieadbf702013-03-14 09:23:06 -040055def url_to_image_name(update_url):
56 """Return the image name based on update_url.
57
58 From a URL like:
59 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
60 return lumpy-release/R27-3837.0.0
61
62 @param update_url: url to the image to update to.
63 @returns a string representing the image name in the update_url.
64
65 """
66 return '/'.join(urlparse.urlparse(update_url).path.split('/')[-2:])
67
68
Sean O'Connor5346e4e2010-08-12 18:49:24 +020069class ChromiumOSUpdater():
Dan Shi0f466e82013-02-22 15:44:58 -080070 """Helper class used to update DUT with image of desired version."""
Dale Curtisa94c19c2011-05-02 15:05:17 -070071 KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
72 KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
73
74
Chris Sosaa3ac2152012-05-23 22:23:13 -070075 def __init__(self, update_url, host=None, local_devserver=False):
Sean O'Connor5346e4e2010-08-12 18:49:24 +020076 self.host = host
77 self.update_url = update_url
Chris Sosa77556d82012-04-05 15:23:14 -070078 self._update_error_queue = multiprocessing.Queue(2)
Chris Sosaa3ac2152012-05-23 22:23:13 -070079 self.local_devserver = local_devserver
80 if not local_devserver:
81 self.update_version = url_to_version(update_url)
82 else:
83 self.update_version = None
Sean Oc053dfe2010-08-23 18:22:26 +020084
Sean O'Connor5346e4e2010-08-12 18:49:24 +020085 def check_update_status(self):
Dale Curtis5c32c722011-05-04 19:24:23 -070086 """Return current status from update-engine."""
87 update_status = self._run(
88 '%s -status 2>&1 | grep CURRENT_OP' % UPDATER_BIN)
Sean O'Connor5346e4e2010-08-12 18:49:24 +020089 return update_status.stdout.strip().split('=')[-1]
90
Sean Oc053dfe2010-08-23 18:22:26 +020091
92 def reset_update_engine(self):
Dale Curtis5c32c722011-05-04 19:24:23 -070093 """Restarts the update-engine service."""
Darin Petkov7d572992010-09-23 10:11:05 -070094 self._run('rm -f %s' % UPDATED_MARKER)
Sean O267c00b2010-08-31 15:54:55 +020095 try:
96 self._run('initctl stop update-engine')
Dale Curtis5c32c722011-05-04 19:24:23 -070097 except error.AutoservRunError:
Sean O267c00b2010-08-31 15:54:55 +020098 logging.warn('Stopping update-engine service failed. Already dead?')
Sean Oc053dfe2010-08-23 18:22:26 +020099 self._run('initctl start update-engine')
Dale Curtis5c32c722011-05-04 19:24:23 -0700100
Sean Oc053dfe2010-08-23 18:22:26 +0200101 if self.check_update_status() != UPDATER_IDLE:
102 raise ChromiumOSError('%s is not in an installable state' %
103 self.host.hostname)
104
105
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200106 def _run(self, cmd, *args, **kwargs):
Dale Curtis5c32c722011-05-04 19:24:23 -0700107 """Abbreviated form of self.host.run(...)"""
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200108 return self.host.run(cmd, *args, **kwargs)
109
Sean Oc053dfe2010-08-23 18:22:26 +0200110
Dale Curtisa94c19c2011-05-02 15:05:17 -0700111 def rootdev(self, options=''):
Dan Shi0f466e82013-02-22 15:44:58 -0800112 """Returns the stripped output of rootdev <options>.
113
114 @param options: options to run rootdev.
115
116 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700117 return self._run('rootdev %s' % options).stdout.strip()
118
119
120 def get_kernel_state(self):
121 """Returns the (<active>, <inactive>) kernel state as a pair."""
122 active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0])
123 if active_root == self.KERNEL_A['root']:
124 return self.KERNEL_A, self.KERNEL_B
125 elif active_root == self.KERNEL_B['root']:
126 return self.KERNEL_B, self.KERNEL_A
127 else:
Dale Curtis5c32c722011-05-04 19:24:23 -0700128 raise ChromiumOSError('Encountered unknown root partition: %s' %
Dale Curtisa94c19c2011-05-02 15:05:17 -0700129 active_root)
130
131
132 def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'):
133 """Return numeric cgpt value for the specified flag, kernel, device. """
134 return int(self._run('cgpt show -n -i %d %s %s' % (
135 kernel['kernel'], flag, dev)).stdout.strip())
136
137
138 def get_kernel_priority(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800139 """Return numeric priority for the specified kernel.
140
141 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
142
143 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700144 return self._cgpt('-P', kernel)
145
146
147 def get_kernel_success(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800148 """Return boolean success flag for the specified kernel.
149
150 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
151
152 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700153 return self._cgpt('-S', kernel) != 0
154
155
156 def get_kernel_tries(self, kernel):
Dan Shi0f466e82013-02-22 15:44:58 -0800157 """Return tries count for the specified kernel.
158
159 @param kernel: information of the given kernel, KERNEL_A or KERNEL_B.
160
161 """
Dale Curtisa94c19c2011-05-02 15:05:17 -0700162 return self._cgpt('-T', kernel)
Sean O267c00b2010-08-31 15:54:55 +0200163
164
Chris Sosa5e4246b2012-05-22 18:05:22 -0700165 def get_stateful_update_script(self):
166 """Returns the path to the stateful update script on the target."""
Chris Sosaa3ac2152012-05-23 22:23:13 -0700167 # We attempt to load the local stateful update path in 3 different
168 # ways. First we use the location specified in the autotest global
169 # config. If this doesn't exist, we attempt to use the Chromium OS
170 # Chroot path to the installed script. If all else fails, we use the
171 # stateful update script on the host.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700172 stateful_update_path = os.path.join(
173 global_config.global_config.get_config_value(
174 'CROS', 'source_tree', default=''),
175 LOCAL_STATEFUL_UPDATE_PATH)
176
Chris Sosaa3ac2152012-05-23 22:23:13 -0700177 if not os.path.exists(stateful_update_path):
178 logging.warn('Could not find Chrome OS source location for '
179 'stateful_update script at %s, falling back to chroot '
180 'copy.', stateful_update_path)
181 stateful_update_path = LOCAL_CHROOT_STATEFUL_UPDATE_PATH
182
183 if not os.path.exists(stateful_update_path):
184 logging.warn('Could not chroot stateful_update script, falling '
185 'back on client copy.')
186 statefuldev_script = REMOTE_STATEUL_UPDATE_PATH
187 else:
Chris Sosa5e4246b2012-05-22 18:05:22 -0700188 self.host.send_file(
189 stateful_update_path, STATEFUL_UPDATE, delete_dest=True)
190 statefuldev_script = STATEFUL_UPDATE
Chris Sosa5e4246b2012-05-22 18:05:22 -0700191
192 return statefuldev_script
193
194
195 def reset_stateful_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800196 """Clear any pending stateful update request."""
Chris Sosa5e4246b2012-05-22 18:05:22 -0700197 statefuldev_cmd = [self.get_stateful_update_script()]
198 statefuldev_cmd += ['--stateful_change=reset', '2>&1']
199 # This shouldn't take any time at all.
200 self._run(' '.join(statefuldev_cmd), timeout=10)
201
202
Sean O267c00b2010-08-31 15:54:55 +0200203 def revert_boot_partition(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800204 """Revert the boot partition."""
Dale Curtisd9b26b92011-10-24 13:34:46 -0700205 part = self.rootdev('-s')
Sean O267c00b2010-08-31 15:54:55 +0200206 logging.warn('Reverting update; Boot partition will be %s', part)
207 return self._run('/postinst %s 2>&1' % part)
208
209
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800210 def trigger_update(self):
211 """Triggers a background update on a test image.
212
213 @raise RootFSUpdateError if anything went wrong.
214
215 """
216 autoupdate_cmd = '%s --check_for_update --omaha_url=%s' % (
217 UPDATER_BIN, self.update_url)
218 logging.info('triggering update via: %s', autoupdate_cmd)
219 try:
220 # This should return immediately, hence the short timeout.
221 self._run(autoupdate_cmd, timeout=10)
222 except error.AutoservRunError, e:
223 raise RootFSUpdateError('update triggering failed on %s: %s' %
224 (self.host.hostname, str(e)))
225
226
Dale Curtis5c32c722011-05-04 19:24:23 -0700227 def _update_root(self):
Chris Sosa77556d82012-04-05 15:23:14 -0700228 logging.info('Updating root partition...')
Dale Curtis5c32c722011-05-04 19:24:23 -0700229
230 # Run update_engine using the specified URL.
231 try:
232 autoupdate_cmd = '%s --update --omaha_url=%s 2>&1' % (
233 UPDATER_BIN, self.update_url)
234 self._run(autoupdate_cmd, timeout=900)
235 except error.AutoservRunError:
Chris Sosa77556d82012-04-05 15:23:14 -0700236 update_error = RootFSUpdateError('update-engine failed on %s' %
237 self.host.hostname)
238 self._update_error_queue.put(update_error)
239 raise update_error
Dale Curtis5c32c722011-05-04 19:24:23 -0700240
241 # Check that the installer completed as expected.
242 status = self.check_update_status()
243 if status != UPDATER_NEED_REBOOT:
Chris Sosa77556d82012-04-05 15:23:14 -0700244 update_error = RootFSUpdateError('update-engine error on %s: %s' %
245 (self.host.hostname, status))
246 self._update_error_queue.put(update_error)
247 raise update_error
Dale Curtis5c32c722011-05-04 19:24:23 -0700248
249
250 def _update_stateful(self):
Chris Sosa77556d82012-04-05 15:23:14 -0700251 logging.info('Updating stateful partition...')
Chris Sosaa3ac2152012-05-23 22:23:13 -0700252 # For production devservers we create a static tree of payloads rooted
253 # at archive.
254 if not self.local_devserver:
255 statefuldev_url = self.update_url.replace('update',
256 'static/archive')
257 else:
258 statefuldev_url = self.update_url.replace('update',
259 'static')
260
Dale Curtis5c32c722011-05-04 19:24:23 -0700261 # Attempt stateful partition update; this must succeed so that the newly
262 # installed host is testable after update.
Chris Sosa5e4246b2012-05-22 18:05:22 -0700263 statefuldev_cmd = [self.get_stateful_update_script()]
Dale Curtis5c32c722011-05-04 19:24:23 -0700264 statefuldev_cmd += [statefuldev_url, '--stateful_change=clean', '2>&1']
265 try:
266 self._run(' '.join(statefuldev_cmd), timeout=600)
267 except error.AutoservRunError:
Chris Sosa77556d82012-04-05 15:23:14 -0700268 update_error = StatefulUpdateError('stateful_update failed on %s' %
269 self.host.hostname)
270 self._update_error_queue.put(update_error)
271 raise update_error
Dale Curtis5c32c722011-05-04 19:24:23 -0700272
273
Dan Shi0f466e82013-02-22 15:44:58 -0800274 def run_update(self, force_update, update_root=True):
275 """Update the DUT with image of specific version.
Chris Sosaa3ac2152012-05-23 22:23:13 -0700276
Dan Shi0f466e82013-02-22 15:44:58 -0800277 @param force_update: True to update DUT even if it's running the same
278 version already.
279 @param update_root: True to force a kernel update. If it's False and
280 force_update is True, stateful update will be used to clean up
281 the DUT.
282
283 """
284 booted_version = self.get_build_id()
285 if (self.check_version() and not force_update):
Dale Curtisa94c19c2011-05-02 15:05:17 -0700286 logging.info('System is already up to date. Skipping update.')
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200287 return False
288
Chris Sosaa3ac2152012-05-23 22:23:13 -0700289 if self.update_version:
290 logging.info('Updating from version %s to %s.',
291 booted_version, self.update_version)
Dale Curtis53d55862011-05-16 12:17:59 -0700292
Dale Curtis5c32c722011-05-04 19:24:23 -0700293 # Check that Dev Server is accepting connections (from autoserv's host).
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200294 # If we can't talk to it, the machine host probably can't either.
295 auserver_host = urlparse.urlparse(self.update_url)[1]
296 try:
297 httplib.HTTPConnection(auserver_host).connect()
Dale Curtis5c32c722011-05-04 19:24:23 -0700298 except IOError:
299 raise ChromiumOSError(
300 'Update server at %s not available' % auserver_host)
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200301
Chris Sosaa3ac2152012-05-23 22:23:13 -0700302 logging.info('Installing from %s to %s', self.update_url,
Chris Sosa77556d82012-04-05 15:23:14 -0700303 self.host.hostname)
304
Chris Sosa5e4246b2012-05-22 18:05:22 -0700305 # Reset update state.
Chris Sosa77556d82012-04-05 15:23:14 -0700306 self.reset_update_engine()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700307 self.reset_stateful_partition()
Sean Oc053dfe2010-08-23 18:22:26 +0200308
Dale Curtis1e973182011-07-12 18:21:36 -0700309 try:
Chris Sosa77556d82012-04-05 15:23:14 -0700310 updaters = [
311 multiprocessing.process.Process(target=self._update_root),
312 multiprocessing.process.Process(target=self._update_stateful)
313 ]
Dan Shi0f466e82013-02-22 15:44:58 -0800314 if not update_root:
315 logging.info('Root update is skipped.')
316 updaters = updaters[1:]
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200317
Chris Sosa77556d82012-04-05 15:23:14 -0700318 # Run the updaters in parallel.
319 for updater in updaters: updater.start()
320 for updater in updaters: updater.join()
321
322 # Re-raise the first error that occurred.
323 if not self._update_error_queue.empty():
324 update_error = self._update_error_queue.get()
325 self.revert_boot_partition()
Chris Sosa5e4246b2012-05-22 18:05:22 -0700326 self.reset_stateful_partition()
Chris Sosa77556d82012-04-05 15:23:14 -0700327 raise update_error
Sean Oc053dfe2010-08-23 18:22:26 +0200328
Dale Curtis1e973182011-07-12 18:21:36 -0700329 logging.info('Update complete.')
330 return True
331 except:
332 # Collect update engine logs in the event of failure.
333 if self.host.job:
334 logging.info('Collecting update engine logs...')
335 self.host.get_file(
336 UPDATER_LOGS, self.host.job.sysinfo.sysinfodir,
337 preserve_perm=False)
338 raise
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200339
340
Dale Curtisa94c19c2011-05-02 15:05:17 -0700341 def check_version(self):
Dan Shi0f466e82013-02-22 15:44:58 -0800342 """Check the image running in DUT has the desired version.
343
344 @returns: True if the DUT's image version matches the version that
345 the autoupdater tries to update to.
346
347 """
Dale Curtisf57a25f2011-05-24 14:40:55 -0700348 booted_version = self.get_build_id()
Dan Shib95bb862013-03-22 16:29:28 -0700349 return (self.update_version and
350 self.update_version.endswith(booted_version))
351
352
353 def check_version_to_confirm_install(self):
354 """Check image running in DUT has the desired version to be installed.
355
356 The method should not be used to check if DUT needs to have a full
357 reimage. Only use it to confirm a image is installed.
358
359 For build from trybot, version retrieved from lsb-release looks like
360 3888.0.2013_03_21_1340. However, for trybot build, self.update_version
361 looks like 'R27-3888.0.0-b711', which does not have the date string and
362 it is complicated to get the date string from update url.
363 Therefore, to make the version verification easiser, the date string is
364 ignored in version retrieved from lsb-release.
365
366 When we are checking if a DUT needs to do a full install, we should NOT
367 use this method to check if the DUT is running the same version, since
368 it may return false positive. For example, if the DUT is running a
369 trybot build with version |3888.0.2013_03_21_1340| and the version to
370 be installed is an official build with version of 3888.0.0,
371 check_version method returns False which is expected. However,
372 check_version_to_confirm_install returns True, which will lead to
373 reimage job fails to install correct image in the DUT.
374
375 On the other hand, when we try to confirm if a build is successfully
376 installed, we should ignore the date string, so a trybot build can be
377 installed successfully when the reimage job calls
378 check_version_to_confirm_install to verify the image version at the end
379 of the job.
380
381 This logic has a bug if a trybot build failed to be installed in a
382 DUT running an older trybot build with same platform number, but
383 different build number (-b###). So to conclusively determine if a
384 tryjob build is imaged successfully, we do need to find out the date
385 string from update url.
386
387 @returns: True if the DUT's image version (without the date string if
388 the image is a trybot build), matches the version that the
389 autoupdater is trying to update to.
390
391 """
392 # Always try the default check_version method first, this prevents
393 # any backward compatibility issue.
394 if self.check_version():
395 return True
396
397 booted_version = self.get_build_id()
398 booted_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '',
399 booted_version)
400 # For a DUT running a build from trybot, only matching the version
401 # number is enough to consider versions are matched.
402 if booted_version != booted_version_no_date:
403 return (self.update_version and
404 booted_version_no_date in self.update_version)
405 else:
406 return (self.update_version and
407 self.update_version.endswith(booted_version_no_date))
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200408
Sean Oc053dfe2010-08-23 18:22:26 +0200409
Sean O'Connor5346e4e2010-08-12 18:49:24 +0200410 def get_build_id(self):
Dale Curtis793f9122011-02-04 15:00:52 -0800411 """Pulls the CHROMEOS_RELEASE_VERSION string from /etc/lsb-release."""
412 return self._run('grep CHROMEOS_RELEASE_VERSION'
413 ' /etc/lsb-release').stdout.split('=')[1].strip()