autoupdater.py revision 5c32c72beab5784abff82d07bf2618946c2445ec
1# Copyright (c) 2011 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 5import httplib 6import logging 7import os 8import re 9import urlparse 10 11from autotest_lib.client.common_lib import error, global_config 12 13# Local stateful update path is relative to the CrOS source directory. 14LOCAL_STATEFUL_UPDATE_PATH = 'src/platform/dev/stateful_update' 15REMOTE_STATEUL_UPDATE_PATH = '/usr/local/bin/stateful_update' 16STATEFUL_UPDATE = '/tmp/stateful_update' 17UPDATER_BIN = '/usr/bin/update_engine_client' 18UPDATER_IDLE = 'UPDATE_STATUS_IDLE' 19UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' 20UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed' 21 22 23class ChromiumOSError(error.InstallError): 24 """Generic error for ChromiumOS-specific exceptions.""" 25 pass 26 27 28def url_to_version(update_url): 29 # The ChromiumOS updater respects the last element in the path as 30 # the requested version. Parse it out. 31 return urlparse.urlparse(update_url).path.split('/')[-1] 32 33 34class ChromiumOSUpdater(): 35 KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3} 36 KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5} 37 38 39 def __init__(self, update_url, host=None): 40 self.host = host 41 self.update_url = update_url 42 self.update_version = url_to_version(update_url) 43 44 45 def check_update_status(self): 46 """Return current status from update-engine.""" 47 update_status = self._run( 48 '%s -status 2>&1 | grep CURRENT_OP' % UPDATER_BIN) 49 return update_status.stdout.strip().split('=')[-1] 50 51 52 def reset_update_engine(self): 53 """Restarts the update-engine service.""" 54 self._run('rm -f %s' % UPDATED_MARKER) 55 try: 56 self._run('initctl stop update-engine') 57 except error.AutoservRunError: 58 logging.warn('Stopping update-engine service failed. Already dead?') 59 self._run('initctl start update-engine') 60 61 if self.check_update_status() != UPDATER_IDLE: 62 raise ChromiumOSError('%s is not in an installable state' % 63 self.host.hostname) 64 65 66 def _run(self, cmd, *args, **kwargs): 67 """Abbreviated form of self.host.run(...)""" 68 return self.host.run(cmd, *args, **kwargs) 69 70 71 def rootdev(self, options=''): 72 """Returns the stripped output of rootdev <options>.""" 73 return self._run('rootdev %s' % options).stdout.strip() 74 75 76 def get_kernel_state(self): 77 """Returns the (<active>, <inactive>) kernel state as a pair.""" 78 active_root = int(re.findall('\d+\Z', self.rootdev('-s'))[0]) 79 if active_root == self.KERNEL_A['root']: 80 return self.KERNEL_A, self.KERNEL_B 81 elif active_root == self.KERNEL_B['root']: 82 return self.KERNEL_B, self.KERNEL_A 83 else: 84 raise ChromiumOSError('Encountered unknown root partition: %s' % 85 active_root) 86 87 88 def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'): 89 """Return numeric cgpt value for the specified flag, kernel, device. """ 90 return int(self._run('cgpt show -n -i %d %s %s' % ( 91 kernel['kernel'], flag, dev)).stdout.strip()) 92 93 94 def get_kernel_priority(self, kernel): 95 """Return numeric priority for the specified kernel.""" 96 return self._cgpt('-P', kernel) 97 98 99 def get_kernel_success(self, kernel): 100 """Return boolean success flag for the specified kernel.""" 101 return self._cgpt('-S', kernel) != 0 102 103 104 def get_kernel_tries(self, kernel): 105 """Return tries count for the specified kernel.""" 106 return self._cgpt('-T', kernel) 107 108 109 def revert_boot_partition(self): 110 part = self.rootdev() 111 logging.warn('Reverting update; Boot partition will be %s', part) 112 return self._run('/postinst %s 2>&1' % part) 113 114 115 def _update_root(self): 116 # Reset update_engine's state & check that update_engine is idle. 117 self.reset_update_engine() 118 119 # Run update_engine using the specified URL. 120 try: 121 autoupdate_cmd = '%s --update --omaha_url=%s 2>&1' % ( 122 UPDATER_BIN, self.update_url) 123 self._run(autoupdate_cmd, timeout=900) 124 except error.AutoservRunError: 125 raise ChromiumOSError('update-engine failed on %s' % 126 self.host.hostname) 127 128 # Check that the installer completed as expected. 129 status = self.check_update_status() 130 if status != UPDATER_NEED_REBOOT: 131 raise ChromiumOSError('update-engine error on %s: %s' % 132 (self.host.hostname, status)) 133 134 135 def _update_stateful(self): 136 # Attempt stateful partition update; this must succeed so that the newly 137 # installed host is testable after update. 138 statefuldev_url = self.update_url.replace('update', 'static/archive') 139 140 # Load the Chrome OS source tree location. 141 stateful_update_path = os.path.join( 142 global_config.global_config.get_config_value( 143 'CROS', 'source_tree', default=''), 144 LOCAL_STATEFUL_UPDATE_PATH) 145 146 if os.path.exists(stateful_update_path): 147 self.host.send_file( 148 stateful_update_path, STATEFUL_UPDATE, delete_dest=True) 149 statefuldev_cmd = [STATEFUL_UPDATE] 150 else: 151 logging.warn('Could not find local stateful_update script, falling' 152 ' back on client copy.') 153 statefuldev_cmd = [REMOTE_STATEUL_UPDATE_PATH] 154 155 statefuldev_cmd += [statefuldev_url, '--stateful_change=clean', '2>&1'] 156 try: 157 self._run(' '.join(statefuldev_cmd), timeout=600) 158 except error.AutoservRunError: 159 self.revert_boot_partition() 160 raise ChromiumOSError('stateful_update failed on %s' % 161 self.host.hostname) 162 163 164 def run_update(self, force_update): 165 booted_version = self.get_booted_version() 166 if booted_version in self.update_version and not force_update: 167 logging.info('System is already up to date. Skipping update.') 168 return False 169 170 # Check that Dev Server is accepting connections (from autoserv's host). 171 # If we can't talk to it, the machine host probably can't either. 172 auserver_host = urlparse.urlparse(self.update_url)[1] 173 try: 174 httplib.HTTPConnection(auserver_host).connect() 175 except IOError: 176 raise ChromiumOSError( 177 'Update server at %s not available' % auserver_host) 178 179 logging.info( 180 'Installing from %s to: %s', self.update_url, self.host.hostname) 181 182 logging.info('Updating root partition...') 183 self._update_root() 184 185 logging.info('Updating stateful partition...') 186 self._update_stateful() 187 188 logging.info('Update complete.') 189 return True 190 191 192 def get_booted_version(self): 193 booted_version = self.get_build_id() 194 if not booted_version: 195 booted_version = self.get_dev_build_id() 196 return booted_version 197 198 199 def check_version(self): 200 booted_version = self.get_booted_version() 201 if not booted_version in self.update_version: 202 logging.error('Expected Chromium OS version: %s.' 203 'Found Chromium OS %s', 204 self.update_version, booted_version) 205 raise ChromiumOSError('Updater failed on host %s' % 206 self.host.hostname) 207 else: 208 return True 209 210 211 def get_build_id(self): 212 """Turns the CHROMEOS_RELEASE_DESCRIPTION into a string that 213 matches the build ID.""" 214 version = self._run('grep CHROMEOS_RELEASE_DESCRIPTION' 215 ' /etc/lsb-release').stdout 216 build_re = (r'CHROMEOS_RELEASE_DESCRIPTION=' 217 '(\d+\.\d+\.\d+\.\d+) \(\w+ \w+ (\w+)(.*)\)') 218 version_match = re.match(build_re, version) 219 if version_match: 220 version, build_id, builder = version_match.groups() 221 build_match = re.match(r'.*: (\d+)', builder) 222 if build_match: 223 builder_num = '-b%s' % build_match.group(1) 224 else: 225 builder_num = '' 226 return '%s-r%s%s' % (version, build_id, builder_num) 227 228 229 def get_dev_build_id(self): 230 """Pulls the CHROMEOS_RELEASE_VERSION string from /etc/lsb-release.""" 231 return self._run('grep CHROMEOS_RELEASE_VERSION' 232 ' /etc/lsb-release').stdout.split('=')[1].strip() 233