autoupdater.py revision a5b75c741c5d0fd13d7255373acd8a89e8eed7fb
1# Copyright (c) 2010 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 re 8import socket 9import time 10import urlparse 11 12from autotest_lib.client.common_lib import error 13 14# TODO(dalecurtis): HACK to bootstrap stateful updater until crosbug.com/8960 is 15# fixed. 16LOCAL_STATEFULDEV_UPDATER = ('/home/chromeos-test/chromeos-src/chromeos/src' 17 '/platform/dev/stateful_update') 18STATEFULDEV_UPDATER = '/tmp/stateful_update' 19UPDATER_BIN = '/usr/bin/update_engine_client' 20UPDATER_IDLE = 'UPDATE_STATUS_IDLE' 21UPDATER_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT' 22UPDATED_MARKER = '/var/run/update_engine_autoupdate_completed' 23 24 25class ChromiumOSError(error.InstallError): 26 """Generic error for ChromiumOS-specific exceptions.""" 27 pass 28 29 30def url_to_version(update_url): 31 # The ChromiumOS updater respects the last element in the path as 32 # the requested version. Parse it out. 33 return urlparse.urlparse(update_url).path.split('/')[-1] 34 35 36class ChromiumOSUpdater(): 37 KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3} 38 KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5} 39 40 41 def __init__(self, update_url, host=None): 42 self.host = host 43 self.update_url = update_url 44 self.update_version = url_to_version(update_url) 45 46 47 def check_update_status(self): 48 update_status_cmd = ' '.join([UPDATER_BIN, '-status', '2>&1', 49 '| grep CURRENT_OP']) 50 update_status = self._run(update_status_cmd) 51 return update_status.stdout.strip().split('=')[-1] 52 53 54 def reset_update_engine(self): 55 logging.info('Resetting update-engine.') 56 self._run('rm -f %s' % UPDATED_MARKER) 57 try: 58 self._run('initctl stop update-engine') 59 except error.AutoservRunError, e: 60 logging.warn('Stopping update-engine service failed. Already dead?') 61 self._run('initctl start update-engine') 62 # May need to wait if service becomes slow to restart. 63 if self.check_update_status() != UPDATER_IDLE: 64 raise ChromiumOSError('%s is not in an installable state' % 65 self.host.hostname) 66 67 68 def _run(self, cmd, *args, **kwargs): 69 return self.host.run(cmd, *args, **kwargs) 70 71 72 def rootdev(self, 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 run_update(self, force_update): 116 booted_version = self.get_booted_version() 117 if booted_version in self.update_version and not force_update: 118 logging.info('System is already up to date. Skipping update.') 119 return False 120 121 # Check that devserver is accepting connections (from autoserv's host) 122 # If we can't talk to it, the machine host probably can't either. 123 auserver_host = urlparse.urlparse(self.update_url)[1] 124 try: 125 httplib.HTTPConnection(auserver_host).connect() 126 except socket.error: 127 raise ChromiumOSError('Update server at %s not available' % 128 auserver_host) 129 130 logging.info('Installing from %s to: %s' % (self.update_url, 131 self.host.hostname)) 132 # Reset update_engine's state & check that update_engine is idle. 133 self.reset_update_engine() 134 # TODO(dalecurtis): Hack! Remove once http://crosbug.com/14954 fixed. 135 time.sleep(120) 136 137 # Run autoupdate command. This tells the autoupdate process on 138 # the host to look for an update at a specific URL and version 139 # string. 140 autoupdate_cmd = ' '.join([UPDATER_BIN, 141 '--update', 142 '--omaha_url=%s' % self.update_url, 143 ' 2>&1']) 144 logging.info(autoupdate_cmd) 145 try: 146 self._run(autoupdate_cmd, timeout=900) 147 except error.AutoservRunError, e: 148 # Either a runtime error occurred on the host, or 149 # update_engine_client exited with > 0. 150 raise ChromiumOSError('update_engine failed on %s' % 151 self.host.hostname) 152 153 # Check that the installer completed as expected. 154 status = self.check_update_status() 155 if status != UPDATER_NEED_REBOOT: 156 raise ChromiumOSError('update-engine error on %s: ' 157 '"%s" from update-engine' % 158 (self.host.hostname, status)) 159 160 # Attempt dev & test tools update (which don't live on the 161 # rootfs). This must succeed so that the newly installed host 162 # is testable after we run the autoupdater. 163 statefuldev_url = self.update_url.replace('update', 'static/archive') 164 165 # TODO(dalecurtis): HACK to bootstrap stateful updater until 166 # crosbug.com/8960 is fixed. 167 self.host.send_file(LOCAL_STATEFULDEV_UPDATER, STATEFULDEV_UPDATER, 168 delete_dest=True) 169 statefuldev_cmd = [STATEFULDEV_UPDATER, statefuldev_url] 170 171 # TODO(dalecurtis): HACK necessary until R10 builds are out of testing. 172 if int(self.update_version.split('.')[1]) > 10: 173 statefuldev_cmd.append('--stateful_change=clean') 174 175 statefuldev_cmd.append('2>&1') 176 statefuldev_cmd = ' '.join(statefuldev_cmd) 177 178 logging.info(statefuldev_cmd) 179 try: 180 self._run(statefuldev_cmd, timeout=600) 181 except error.AutoservRunError, e: 182 # TODO(seano): If statefuldev update failed, we must mark 183 # the update as failed, and keep the same rootfs after 184 # reboot. 185 self.revert_boot_partition() 186 raise ChromiumOSError('stateful_update failed on %s.' % 187 self.host.hostname) 188 return True 189 190 191 def get_booted_version(self): 192 booted_version = self.get_build_id() 193 if not booted_version: 194 booted_version = self.get_dev_build_id() 195 return booted_version 196 197 198 def check_version(self): 199 booted_version = self.get_booted_version() 200 if not booted_version in self.update_version: 201 logging.error('Expected Chromium OS version: %s.' 202 'Found Chromium OS %s', 203 self.update_version, booted_version) 204 raise ChromiumOSError('Updater failed on host %s' % 205 self.host.hostname) 206 else: 207 return True 208 209 210 def get_build_id(self): 211 """Turns the CHROMEOS_RELEASE_DESCRIPTION into a string that 212 matches the build ID.""" 213 version = self._run('grep CHROMEOS_RELEASE_DESCRIPTION' 214 ' /etc/lsb-release').stdout 215 build_re = (r'CHROMEOS_RELEASE_DESCRIPTION=' 216 '(\d+\.\d+\.\d+\.\d+) \(\w+ \w+ (\w+)(.*)\)') 217 version_match = re.match(build_re, version) 218 if version_match: 219 version, build_id, builder = version_match.groups() 220 build_match = re.match(r'.*: (\d+)', builder) 221 if build_match: 222 builder_num = '-b%s' % build_match.group(1) 223 else: 224 builder_num = '' 225 return '%s-r%s%s' % (version, build_id, builder_num) 226 227 228 def get_dev_build_id(self): 229 """Pulls the CHROMEOS_RELEASE_VERSION string from /etc/lsb-release.""" 230 return self._run('grep CHROMEOS_RELEASE_VERSION' 231 ' /etc/lsb-release').stdout.split('=')[1].strip() 232